Exercice 1 (Cas séparable).

Etant donné un échantillon séparable \((x_i,y_i),i=1,\dots,n\)\(x_i\in\mathbb R^2\) et \(y_i\in\{0,1\}\), on rappelle que l’algorithme SVM consiste à trouver un hyperplan \[\langle w,x\rangle+b=0\] qui sépare les \(x_i\) en fonction des \(y_i\). On génère des données selon

library(tidyverse)
n <- 20
set.seed(123)
X1 <- scale(runif(n))
set.seed(567)
X2 <- scale(runif(n))
Y <- rep(0,n)
Y[X1>X2] <- 1
Y <- as.factor(Y)
donnees <- data.frame(X1=X1,X2=X2,Y=Y)

Et on considère la svm suivante :

library(e1071)
mod.svm <- svm(Y~.,data=donnees,kernel="linear",cost=10000000000)
  1. Représenter le nuage de points en utilisant une couleur différente selon la valeur de \(Y\).
p <- ggplot(donnees)+aes(x=X2,y=X1,color=Y)+geom_point()+theme_classic()
p
  1. Récupérer les vecteurs supports et ajouter les sur le graphe. On les affectera à un data.frame dont les 2 premières colonnes représenteront les valeurs de \(X_1\) et \(X_2\) des vecteurs supports.

Les vecteurs supports se trouvent dans l’élément index de la fonction svm :

ind.svm <- mod.svm$index
sv <- donnees %>% slice(ind.svm)
sv
p1 <- p+geom_point(data=sv,aes(x=X2,y=X1),color="blue",size=2)

On peut ainsi représenter la marge en traçant les droites qui passent par ces points.

sv1 <- sv[,2:1]
b <- (sv1[1,2]-sv1[2,2])/(sv1[1,1]-sv1[2,1])
a <- sv1[1,2]-b*sv1[1,1]
a1 <- sv1[3,2]-b*sv1[3,1]
p1+geom_abline(intercept = c(a,a1),slope=b,col="blue",size=1)
  1. Retrouver ce graphe à l’aide de la fonction plot.
plot(mod.svm,data=donnees,grid=250)
  1. Rappeler la règle de décision associée la méthode SVM. Donner les estimations des paramètres de la règle de décision sur cet exemple.

L’hyperplan séparateur est d’équation

\[\langle w^\star,x\rangle+b^\star=0\] avec

\[w^\star=\sum_{i=1}^n\alpha_i^\star y_ix_i\] et \(b^\star\) solution de \(y_i<w^\star,x_i>+b=0\) (pour \(\alpha_i^\star\neq 0\)). La règle s’écrit donc \[g(x)=1_{<w^\star,x>+b^\star\leq 0}-1_{<w^\star,x>+b^\star> 0}.\] L’élément mod.svm$coefs contient les coefficients \(\alpha_i^\star y_i\) pour chaque vecteur support. On peut ainsi récupérer l’équation de l’hyperplan et faire la prévision avec

w <- apply(mod.svm$coefs*donnees[mod.svm$index,1:2],2,sum)
w
b <- -mod.svm$rho

L’hyperplan séparateur a donc pour équation : \[-1.74x_1+2.12x_2-0.40=0.\]

  1. On dispose d’un nouvel individu \(x=(-0.5,0.5)\). Expliquer comment on peut prédire son groupe.

Il suffit de calculer \(<w^\star,x>+b\) et de prédire en fonction du signe de cette valeur :

newX <- data.frame(X1=-0.5,X2=0.5)
sum(w*newX)+b

On prédira le groupe 0 pour ce nouvel individu.

  1. Retrouver les résultats de la question précédente à l’aide de la fonction predict. On pourra utiliser l’option decision.values = TRUE.
predict(mod.svm,newX,decision.values = TRUE)

Plus cette valeur est élevée, plus on est loin de l’hyperplan. On peut donc l’interpréter comme un score. Comme souvent, il est possible d’obtenir une estimation des probabilités d’être dans les groupes 0 et 1 à partir de ce score, il “suffit” de ramener ce score sur l’échelle \([0,1]\) avec des transformations de type logit par exemple. Pour la svm, ces probabilités sont obtenues en ajustant un modèle logistique sur les scores \(S(x)\) : \[P(Y=1|X=x)=\frac{1}{1+\exp(aS(x)+b)}.\]

  1. Obtenir ces probabilités à l’aide de la fonction predict. On pourra utiliser probability=TRUE dans la fonction svm.
mod.svm1 <- svm(Y~.,data=donnees,kernel="linear",cost=10000000000,probability=TRUE)
predict(mod.svm1,newX,decision.values=TRUE,probability=TRUE)

On peut retrouver ces probabilités avec :

score.newX <- sum(w*newX)+b
1/(1+exp(-(mod.svm1$probB+mod.svm1$probA*score.newX)))

Exercice 2 (cas non séparable).

On considère le jeu de données suivant où le problème est d’expliquer \(Y\) par \(V1\) et \(V2\).

n <- 750
set.seed(1)
X <- matrix(runif(n*2,-2,2),ncol=2) %>% as.data.frame()
Y <- rep(0,n)
cond <- (X$V1^2+X$V2^2)<=2.8
Y[cond] <- rbinom(sum(cond),1,0.9)
Y[!cond] <- rbinom(sum(!cond),1,0.1)
df <- X %>% mutate(Y=as.factor(Y))
ggplot(df)+aes(x=V1,y=V2,color=Y)+geom_point()+theme_classic()
  1. Séparer l’échantillon en un échantillon d’apprentissage de taille 500 et un échantillon test de taille 250.
set.seed(1234)
ind.train <- sample(nrow(df),500)
train <- df %>% slice(ind.train)
test <- df %>% slice(-ind.train)
  1. Ajuster une svm linéaire sur l’échantillon d’apprentissage et visualiser l’hyperplan séparateur. Que remarquez-vous ?
mod.svm0 <- svm(Y~.,data=train,kernel="linear",cost=1)
plot(mod.svm0,train,grid=250)

La svm linéaire ne permet pas de séparer les groupes (on pouvait s’y attendre).

  1. En quoi consiste l’astuce noyau pour les svm ?

L’astuce du noyau consiste à envoyer les données dans un espace de représentation (feature space) dans lequel on espère que les données soient linéairement séparables.

  1. Exécuter la commande suivante et commenter la sortie.
mod.svm1 <- svm(Y~.,data=train,kernel="radial",gamma=1,cost=1)
plot(mod.svm1,train,grid=250)

Le noyau radial permet de mettre en évidence une séparation non linéaire.

  1. Faire varier les paramètres gamma et cost. Interpréter (on pourra notamment étudier l’évolution du nombre de vecteurs supports en fonction du paramètre cost).
mod.svm2 <- svm(Y~.,data=train,kernel="radial",gamma=1,cost=0.0001)
mod.svm3 <- svm(Y~.,data=train,kernel="radial",gamma=1,cost=1)
mod.svm4 <- svm(Y~.,data=train,kernel="radial",gamma=1,cost=10000)

plot(mod.svm2,train,grid=250)
plot(mod.svm3,train,grid=250)
plot(mod.svm4,train,grid=250)

mod.svm2$nSV
mod.svm3$nSV
mod.svm4$nSV

Le nombre de vecteurs supports diminue lorsque \(C\) augmente. Une forte valeur de \(C\) autorise moins d’observations à être dans la marge, elle a donc tendance à diminuer (risque de surapprentissage).

  1. Sélectionner automatiquement ces paramètres. On pourra utiliser la fonction tune en faisant varier C dans c(0.1,1,10,100,1000) et gamma dans c(0.5,1,2,3,4).
set.seed(1234)
tune.out <- tune(svm,Y~.,data=train,kernel="radial",ranges=list(cost=c(0.1,1,10,100,1000),gamma=c(0.5,1,2,3,4)))
summary(tune.out)

La sélection est faite en minimisant l’erreur de classification par validation croisée 10 blocs.

  1. Faire de même avec caret, on utilisera method=“svmRadial” et prob.model=TRUE.

Pour caret il faut utiliser la méthode svmRadial du package kernlab.

library(caret)
library(kernlab)
C <- c(0.001,0.01,1,10,100,1000)
sigma <- c(0.5,1,2,3,4)
gr <- expand.grid(C=C,sigma=sigma)
ctrl <- trainControl(method="cv")
res.caret1 <- train(Y~.,data=train,method="svmRadial",trControl=ctrl,tuneGrid=gr,prob.model=TRUE)
res.caret1

On peut également répéter plusieurs fois la validation croisée pour stabiliser les résultats (on parallélise avec doParallel) :

library(doParallel) ## pour paralléliser
cl <- makePSOCKcluster(4)
registerDoParallel(cl)
set.seed(12345)
ctrl <- trainControl(method="repeatedcv",number=10,repeats=5)
res.caret2 <- train(Y~.,data=train,method="svmRadial",trControl=ctrl,tuneGrid=gr,prob.model=TRUE)
on.exit(stopCluster(cl))
res.caret2
  1. Comparer la svm sélectionnée à la svm linéaire à l’aide de la courbe ROC et de l’erreur de classification. On pourra créer une table qui contient les scores et les labels observés des individus de l’échantillon test. On pourra également ajouter une svm polynomiale.
scale <- 1
C <- c(1,10,100)
degree <- c(1:3)
gr <- expand.grid(C=C,scale=scale,degree=degree)
ctrl <- trainControl(method="repeatedcv",number=10,repeats=5)
cl <- makePSOCKcluster(4)
registerDoParallel(cl)
set.seed(12345)
res.caret3 <- train(Y~.,data=train,method="svmPoly",trControl=ctrl,tuneGrid=gr,prob.model=TRUE)
on.exit(stopCluster(cl))
mod.svm0 <- svm(Y~.,data=train,kernel="linear",cost=1,probability=TRUE)
prev.lin <- predict(mod.svm0,newdata=test,probability=TRUE)
prev.lin <- attr(prev.lin, "probabilities")[,2]
prev.rad <- predict(res.caret2,newdata=test,type="prob")[,2]
prev.poly <- predict(res.caret3,newdata=test,type="prob")[,2]
score <- data.frame(lin=prev.lin,rad=prev.rad,poly=prev.poly,obs=as.numeric(as.character(test$Y))) %>% 
  gather(key="Method",value="score",-obs) %>% 
  mutate(prev=recode(as.character(score>0.5),"TRUE"=1,"FALSE"=0))
  • Courbe ROC
library(plotROC)
ggplot(score)+aes(d=obs,m=score,color=Method)+geom_roc()+theme_classic()
  • AUC
score %>% group_by(Method) %>% summarize(AUC=pROC::auc(obs,score)) %>% arrange(desc(AUC))
  • Erreur de classification
score %>% group_by(Method) %>% summarize(Erreur=mean(prev!=obs)) %>% arrange(Erreur)
  1. A l’aide de caret, sélectionner les paramètres de la svm en optimisant l’AUC.

On peut utliser l’option metric de la fonction train :

ctrl <- trainControl(method="cv",classProbs = TRUE,summary = twoClassSummary)
train1 <- train %>% mutate(Y=fct_recode(Y,G0="0",G1="1"))
res.caret4 <- train(Y~.,data=train1,method="svmPoly",trControl=ctrl,tuneGrid=gr,prob.model=TRUE,metric="ROC")
res.caret4
LS0tCnRpdGxlOiAiVFAgOiBzdm0iCm91dHB1dDoKICBodG1sX25vdGVib29rOiAKICAgIGNzczogfi9Ecm9wYm94L0ZJQ0hJRVJTX1NUWUxFL3N0eWxlcy5jc3MKICAgIHRvYzogeWVzCiAgICB0b2NfZmxvYXQ6IHllcwotLS0KCiMjIEV4ZXJjaWNlIDEgKENhcyBzw6lwYXJhYmxlKS4KCkV0YW50IGRvbm7DqSB1biDDqWNoYW50aWxsb24gc8OpcGFyYWJsZSAkKHhfaSx5X2kpLGk9MSxcZG90cyxuJCBvw7kgJHhfaVxpblxtYXRoYmIgUl4yJCBldCAkeV9pXGluXHswLDFcfSQsIG9uIHJhcHBlbGxlIHF1ZSBsJ2FsZ29yaXRobWUgU1ZNIGNvbnNpc3RlIMOgIHRyb3V2ZXIgdW4gaHlwZXJwbGFuIAokJFxsYW5nbGUgdyx4XHJhbmdsZStiPTAkJCAKcXVpIHPDqXBhcmUgbGVzICR4X2kkIGVuIGZvbmN0aW9uIGRlcyAkeV9pJC4gT24gZ8OpbsOocmUgZGVzIGRvbm7DqWVzIHNlbG9uCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbiA8LSAyMApzZXQuc2VlZCgxMjMpClgxIDwtIHNjYWxlKHJ1bmlmKG4pKQpzZXQuc2VlZCg1NjcpClgyIDwtIHNjYWxlKHJ1bmlmKG4pKQpZIDwtIHJlcCgwLG4pCllbWDE+WDJdIDwtIDEKWSA8LSBhcy5mYWN0b3IoWSkKZG9ubmVlcyA8LSBkYXRhLmZyYW1lKFgxPVgxLFgyPVgyLFk9WSkKYGBgCgpFdCBvbiBjb25zaWTDqHJlIGxhICoqc3ZtKiogc3VpdmFudGUgOgoKYGBge3J9CmxpYnJhcnkoZTEwNzEpCm1vZC5zdm0gPC0gc3ZtKFl+LixkYXRhPWRvbm5lZXMsa2VybmVsPSJsaW5lYXIiLGNvc3Q9MTAwMDAwMDAwMDApCmBgYAoKMS4gUmVwcsOpc2VudGVyIGxlIG51YWdlIGRlIHBvaW50cyBlbiB1dGlsaXNhbnQgdW5lIGNvdWxldXIgZGlmZsOpcmVudGUgc2Vsb24gbGEgdmFsZXVyIGRlICRZJC4gCgpgYGB7cn0KcCA8LSBnZ3Bsb3QoZG9ubmVlcykrYWVzKHg9WDIseT1YMSxjb2xvcj1ZKStnZW9tX3BvaW50KCkrdGhlbWVfY2xhc3NpYygpCnAKYGBgCgoKMi4gUsOpY3Vww6lyZXIgbGVzIHZlY3RldXJzIHN1cHBvcnRzIGV0IGFqb3V0ZXIgbGVzIHN1ciBsZSBncmFwaGUuIE9uIGxlcyBhZmZlY3RlcmEgw6AgdW4gKipkYXRhLmZyYW1lKiogZG9udCBsZXMgMiBwcmVtacOocmVzIGNvbG9ubmVzIHJlcHLDqXNlbnRlcm9udCBsZXMgdmFsZXVycyBkZSAkWF8xJCBldCAkWF8yJCBkZXMgdmVjdGV1cnMgc3VwcG9ydHMuCgoKTGVzIHZlY3RldXJzIHN1cHBvcnRzIHNlIHRyb3V2ZW50IGRhbnMgbCfDqWzDqW1lbnQgKippbmRleCoqIGRlIGxhIGZvbmN0aW9uICoqc3ZtKiogOgpgYGB7cn0KaW5kLnN2bSA8LSBtb2Quc3ZtJGluZGV4CnN2IDwtIGRvbm5lZXMgJT4lIHNsaWNlKGluZC5zdm0pCnN2CmBgYAoKYGBge3J9CnAxIDwtIHArZ2VvbV9wb2ludChkYXRhPXN2LGFlcyh4PVgyLHk9WDEpLGNvbG9yPSJibHVlIixzaXplPTIpCmBgYAoKT24gcGV1dCBhaW5zaSByZXByw6lzZW50ZXIgbGEgbWFyZ2UgZW4gdHJhw6dhbnQgbGVzIGRyb2l0ZXMgcXVpIHBhc3NlbnQgcGFyIGNlcyBwb2ludHMuCgpgYGB7cn0Kc3YxIDwtIHN2WywyOjFdCmIgPC0gKHN2MVsxLDJdLXN2MVsyLDJdKS8oc3YxWzEsMV0tc3YxWzIsMV0pCmEgPC0gc3YxWzEsMl0tYipzdjFbMSwxXQphMSA8LSBzdjFbMywyXS1iKnN2MVszLDFdCnAxK2dlb21fYWJsaW5lKGludGVyY2VwdCA9IGMoYSxhMSksc2xvcGU9Yixjb2w9ImJsdWUiLHNpemU9MSkKYGBgCgozLiBSZXRyb3V2ZXIgY2UgZ3JhcGhlIMOgIGwnYWlkZSBkZSBsYSBmb25jdGlvbiAqKnBsb3QqKi4KCmBgYHtyfQpwbG90KG1vZC5zdm0sZGF0YT1kb25uZWVzLGdyaWQ9MjUwKQpgYGAKCgo0LiBSYXBwZWxlciBsYSByw6hnbGUgZGUgZMOpY2lzaW9uIGFzc29jacOpZSBsYSBtw6l0aG9kZSBTVk0uIERvbm5lciBsZXMgZXN0aW1hdGlvbnMgZGVzIHBhcmFtw6h0cmVzIGRlIGxhIHLDqGdsZSBkZSBkw6ljaXNpb24gc3VyIGNldCBleGVtcGxlLgoKTCdoeXBlcnBsYW4gc8OpcGFyYXRldXIgZXN0IGQnw6lxdWF0aW9uCgokJFxsYW5nbGUgd15cc3Rhcix4XHJhbmdsZStiXlxzdGFyPTAkJAphdmVjCgokJHdeXHN0YXI9XHN1bV97aT0xfV5uXGFscGhhX2leXHN0YXIgeV9peF9pJCQKZXQgJGJeXHN0YXIkIHNvbHV0aW9uIGRlICR5X2k8d15cc3Rhcix4X2k+K2I9MCQgKHBvdXIgJFxhbHBoYV9pXlxzdGFyXG5lcSAwJCkuIExhIHLDqGdsZSBzJ8OpY3JpdCBkb25jCiQkZyh4KT0xX3s8d15cc3Rhcix4PitiXlxzdGFyXGxlcSAwfS0xX3s8d15cc3Rhcix4PitiXlxzdGFyPiAwfS4kJApMJ8OpbMOpbWVudCAqKm1vZC5zdm0kY29lZnMqKiBjb250aWVudCBsZXMgY29lZmZpY2llbnRzICRcYWxwaGFfaV5cc3RhciB5X2kkIHBvdXIgY2hhcXVlIHZlY3RldXIgc3VwcG9ydC4gT24gcGV1dCBhaW5zaSByw6ljdXDDqXJlciBsJ8OpcXVhdGlvbiBkZSBsJ2h5cGVycGxhbiBldCBmYWlyZSBsYSBwcsOpdmlzaW9uIGF2ZWMKYGBge3J9CncgPC0gYXBwbHkobW9kLnN2bSRjb2Vmcypkb25uZWVzW21vZC5zdm0kaW5kZXgsMToyXSwyLHN1bSkKdwpiIDwtIC1tb2Quc3ZtJHJobwpgYGAKTCdoeXBlcnBsYW4gc8OpcGFyYXRldXIgYSBkb25jIHBvdXIgw6lxdWF0aW9uIDoKJCQtMS43NHhfMSsyLjEyeF8yLTAuNDA9MC4kJAoKCjUuIE9uIGRpc3Bvc2UgZCd1biBub3V2ZWwgaW5kaXZpZHUgJHg9KC0wLjUsMC41KSQuIEV4cGxpcXVlciBjb21tZW50IG9uIHBldXQgcHLDqWRpcmUgc29uIGdyb3VwZS4KCklsIHN1ZmZpdCBkZSBjYWxjdWxlciAgJDx3XlxzdGFyLHg+K2IkIGV0IGRlIHByw6lkaXJlIGVuIGZvbmN0aW9uIGR1IHNpZ25lIGRlIGNldHRlIHZhbGV1ciA6CgpgYGB7cn0KbmV3WCA8LSBkYXRhLmZyYW1lKFgxPS0wLjUsWDI9MC41KQpzdW0odypuZXdYKStiCmBgYAoKT24gcHLDqWRpcmEgbGUgZ3JvdXBlIDAgcG91ciBjZSBub3V2ZWwgaW5kaXZpZHUuCgo2LiBSZXRyb3V2ZXIgbGVzIHLDqXN1bHRhdHMgZGUgbGEgcXVlc3Rpb24gcHLDqWPDqWRlbnRlIMOgIGwnYWlkZSBkZSBsYSBmb25jdGlvbiAqKnByZWRpY3QqKi4gT24gcG91cnJhIHV0aWxpc2VyIGwnb3B0aW9uIGBkZWNpc2lvbi52YWx1ZXMgPSBUUlVFYC4KCmBgYHtyfQpwcmVkaWN0KG1vZC5zdm0sbmV3WCxkZWNpc2lvbi52YWx1ZXMgPSBUUlVFKQpgYGAKClBsdXMgY2V0dGUgdmFsZXVyIGVzdCDDqWxldsOpZSwgcGx1cyBvbiBlc3QgbG9pbiBkZSBsJ2h5cGVycGxhbi4gT24gcGV1dCBkb25jIGwnaW50ZXJwcsOpdGVyIGNvbW1lIHVuIHNjb3JlLiBDb21tZSBzb3V2ZW50LCBpbCBlc3QgcG9zc2libGUgZCdvYnRlbmlyIHVuZSBlc3RpbWF0aW9uIGRlcyBwcm9iYWJpbGl0w6lzIGQnw6p0cmUgZGFucyBsZXMgZ3JvdXBlcyAwIGV0IDEgw6AgcGFydGlyIGRlIGNlIHNjb3JlLCBpbCAic3VmZml0IiBkZSByYW1lbmVyIGNlIHNjb3JlIHN1ciBsJ8OpY2hlbGxlICRbMCwxXSQgYXZlYyBkZXMgdHJhbnNmb3JtYXRpb25zIGRlIHR5cGUgbG9naXQgcGFyIGV4ZW1wbGUuIFBvdXIgbGEgc3ZtLCBjZXMgcHJvYmFiaWxpdMOpcyBzb250IG9idGVudWVzIGVuIGFqdXN0YW50IHVuIG1vZMOobGUgbG9naXN0aXF1ZSBzdXIgbGVzIHNjb3JlcyAkUyh4KSQgOgokJFAoWT0xfFg9eCk9XGZyYWN7MX17MStcZXhwKGFTKHgpK2IpfS4kJAoKNy4gT2J0ZW5pciBjZXMgcHJvYmFiaWxpdMOpcyDDoCBsJ2FpZGUgZGUgbGEgZm9uY3Rpb24gKipwcmVkaWN0KiouIE9uIHBvdXJyYSB1dGlsaXNlciBgcHJvYmFiaWxpdHk9VFJVRWAgZGFucyBsYSBmb25jdGlvbiAqKnN2bSoqLgoKYGBge3J9Cm1vZC5zdm0xIDwtIHN2bShZfi4sZGF0YT1kb25uZWVzLGtlcm5lbD0ibGluZWFyIixjb3N0PTEwMDAwMDAwMDAwLHByb2JhYmlsaXR5PVRSVUUpCnByZWRpY3QobW9kLnN2bTEsbmV3WCxkZWNpc2lvbi52YWx1ZXM9VFJVRSxwcm9iYWJpbGl0eT1UUlVFKQpgYGAKCk9uIHBldXQgcmV0cm91dmVyIGNlcyBwcm9iYWJpbGl0w6lzIGF2ZWMgOgoKYGBge3J9CnNjb3JlLm5ld1ggPC0gc3VtKHcqbmV3WCkrYgoxLygxK2V4cCgtKG1vZC5zdm0xJHByb2JCK21vZC5zdm0xJHByb2JBKnNjb3JlLm5ld1gpKSkKYGBgCgoKCiMjIEV4ZXJjaWNlIDIgKGNhcyBub24gc8OpcGFyYWJsZSkuCgpPbiBjb25zaWTDqHJlIGxlIGpldSBkZSBkb25uw6llcyBzdWl2YW50IG/DuSBsZSBwcm9ibMOobWUgZXN0IGQnZXhwbGlxdWVyICRZJCBwYXIgJFYxJCBldCAkVjIkLgoKYGBge3J9Cm4gPC0gNzUwCnNldC5zZWVkKDEpClggPC0gbWF0cml4KHJ1bmlmKG4qMiwtMiwyKSxuY29sPTIpICU+JSBhcy5kYXRhLmZyYW1lKCkKWSA8LSByZXAoMCxuKQpjb25kIDwtIChYJFYxXjIrWCRWMl4yKTw9Mi44CllbY29uZF0gPC0gcmJpbm9tKHN1bShjb25kKSwxLDAuOSkKWVshY29uZF0gPC0gcmJpbm9tKHN1bSghY29uZCksMSwwLjEpCmRmIDwtIFggJT4lIG11dGF0ZShZPWFzLmZhY3RvcihZKSkKYGBgCgoKYGBge3J9CmdncGxvdChkZikrYWVzKHg9VjEseT1WMixjb2xvcj1ZKStnZW9tX3BvaW50KCkrdGhlbWVfY2xhc3NpYygpCmBgYAoKCgoKMS4gU8OpcGFyZXIgbCfDqWNoYW50aWxsb24gZW4gdW4gw6ljaGFudGlsbG9uIGQnYXBwcmVudGlzc2FnZSBkZSB0YWlsbGUgNTAwIGV0IHVuIMOpY2hhbnRpbGxvbiB0ZXN0IGRlIHRhaWxsZSAyNTAuCgoKYGBge3J9CnNldC5zZWVkKDEyMzQpCmluZC50cmFpbiA8LSBzYW1wbGUobnJvdyhkZiksNTAwKQp0cmFpbiA8LSBkZiAlPiUgc2xpY2UoaW5kLnRyYWluKQp0ZXN0IDwtIGRmICU+JSBzbGljZSgtaW5kLnRyYWluKQpgYGAKCgoyLiBBanVzdGVyIHVuZSBzdm0gbGluw6lhaXJlIHN1ciBsJ8OpY2hhbnRpbGxvbiBkJ2FwcHJlbnRpc3NhZ2UgZXQgdmlzdWFsaXNlciBsJ2h5cGVycGxhbiBzw6lwYXJhdGV1ci4gUXVlIHJlbWFycXVlei12b3VzID8KCmBgYHtyfQptb2Quc3ZtMCA8LSBzdm0oWX4uLGRhdGE9dHJhaW4sa2VybmVsPSJsaW5lYXIiLGNvc3Q9MSkKcGxvdChtb2Quc3ZtMCx0cmFpbixncmlkPTI1MCkKYGBgCkxhIHN2bSBsaW7DqWFpcmUgbmUgcGVybWV0IHBhcyBkZSBzw6lwYXJlciBsZXMgZ3JvdXBlcyAob24gcG91dmFpdCBzJ3kgYXR0ZW5kcmUpLgoKMy4gRW4gcXVvaSBjb25zaXN0ZSBsJ2FzdHVjZSBub3lhdSBwb3VyIGxlcyBzdm0gPwoKTCdhc3R1Y2UgZHUgbm95YXUgY29uc2lzdGUgw6AgZW52b3llciBsZXMgZG9ubsOpZXMgZGFucyB1biBlc3BhY2UgZGUgcmVwcsOpc2VudGF0aW9uIChmZWF0dXJlIHNwYWNlKSBkYW5zIGxlcXVlbCBvbiBlc3DDqHJlIHF1ZSBsZXMgZG9ubsOpZXMgc29pZW50IGxpbsOpYWlyZW1lbnQgc8OpcGFyYWJsZXMuCgo0LiBFeMOpY3V0ZXIgbGEgY29tbWFuZGUgc3VpdmFudGUgZXQgY29tbWVudGVyIGxhIHNvcnRpZS4KCmBgYHtyfQptb2Quc3ZtMSA8LSBzdm0oWX4uLGRhdGE9dHJhaW4sa2VybmVsPSJyYWRpYWwiLGdhbW1hPTEsY29zdD0xKQpwbG90KG1vZC5zdm0xLHRyYWluLGdyaWQ9MjUwKQpgYGAKCkxlIG5veWF1IHJhZGlhbCBwZXJtZXQgZGUgbWV0dHJlIGVuIMOpdmlkZW5jZSB1bmUgc8OpcGFyYXRpb24gbm9uIGxpbsOpYWlyZS4KCjUuIEZhaXJlIHZhcmllciBsZXMgcGFyYW3DqHRyZXMgKipnYW1tYSoqIGV0ICoqY29zdCoqLiBJbnRlcnByw6l0ZXIgKG9uIHBvdXJyYSBub3RhbW1lbnQgw6l0dWRpZXIgbCfDqXZvbHV0aW9uIGR1IG5vbWJyZSBkZSB2ZWN0ZXVycyBzdXBwb3J0cyBlbiBmb25jdGlvbiBkdSBwYXJhbcOodHJlICoqY29zdCoqKS4KCmBgYHtyfQptb2Quc3ZtMiA8LSBzdm0oWX4uLGRhdGE9dHJhaW4sa2VybmVsPSJyYWRpYWwiLGdhbW1hPTEsY29zdD0wLjAwMDEpCm1vZC5zdm0zIDwtIHN2bShZfi4sZGF0YT10cmFpbixrZXJuZWw9InJhZGlhbCIsZ2FtbWE9MSxjb3N0PTEpCm1vZC5zdm00IDwtIHN2bShZfi4sZGF0YT10cmFpbixrZXJuZWw9InJhZGlhbCIsZ2FtbWE9MSxjb3N0PTEwMDAwKQoKcGxvdChtb2Quc3ZtMix0cmFpbixncmlkPTI1MCkKcGxvdChtb2Quc3ZtMyx0cmFpbixncmlkPTI1MCkKcGxvdChtb2Quc3ZtNCx0cmFpbixncmlkPTI1MCkKCm1vZC5zdm0yJG5TVgptb2Quc3ZtMyRuU1YKbW9kLnN2bTQkblNWCgpgYGAKCkxlIG5vbWJyZSBkZSB2ZWN0ZXVycyBzdXBwb3J0cyBkaW1pbnVlIGxvcnNxdWUgJEMkIGF1Z21lbnRlLiBVbmUgZm9ydGUgdmFsZXVyIGRlICRDJCBhdXRvcmlzZSBtb2lucyBkJ29ic2VydmF0aW9ucyDDoCDDqnRyZSBkYW5zIGxhIG1hcmdlLCBlbGxlIGEgZG9uYyB0ZW5kYW5jZSDDoCBkaW1pbnVlciAocmlzcXVlIGRlIHN1cmFwcHJlbnRpc3NhZ2UpLgoKNi4gU8OpbGVjdGlvbm5lciBhdXRvbWF0aXF1ZW1lbnQgY2VzIHBhcmFtw6h0cmVzLiBPbiBwb3VycmEgdXRpbGlzZXIgbGEgZm9uY3Rpb24gKip0dW5lKiogZW4gZmFpc2FudCB2YXJpZXIgKipDKiogZGFucyAqKmMoMC4xLDEsMTAsMTAwLDEwMDApKiogZXQgKipnYW1tYSoqIGRhbnMgKipjKDAuNSwxLDIsMyw0KSoqLgoKYGBge3J9CnNldC5zZWVkKDEyMzQpCnR1bmUub3V0IDwtIHR1bmUoc3ZtLFl+LixkYXRhPXRyYWluLGtlcm5lbD0icmFkaWFsIixyYW5nZXM9bGlzdChjb3N0PWMoMC4xLDEsMTAsMTAwLDEwMDApLGdhbW1hPWMoMC41LDEsMiwzLDQpKSkKc3VtbWFyeSh0dW5lLm91dCkKYGBgCgpMYSBzw6lsZWN0aW9uIGVzdCBmYWl0ZSBlbiBtaW5pbWlzYW50IGwnZXJyZXVyIGRlIGNsYXNzaWZpY2F0aW9uIHBhciB2YWxpZGF0aW9uIGNyb2lzw6llIDEwIGJsb2NzLgoKNy4gRmFpcmUgZGUgbcOqbWUgYXZlYyAqKmNhcmV0KiosIG9uIHV0aWxpc2VyYSAqKm1ldGhvZD0ic3ZtUmFkaWFsIioqIGV0ICoqcHJvYi5tb2RlbD1UUlVFKiouCgpQb3VyIGNhcmV0IGlsIGZhdXQgdXRpbGlzZXIgbGEgbcOpdGhvZGUgKipzdm1SYWRpYWwqKiBkdSBwYWNrYWdlICprZXJubGFiKi4KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShjYXJldCkKbGlicmFyeShrZXJubGFiKQpDIDwtIGMoMC4wMDEsMC4wMSwxLDEwLDEwMCwxMDAwKQpzaWdtYSA8LSBjKDAuNSwxLDIsMyw0KQpnciA8LSBleHBhbmQuZ3JpZChDPUMsc2lnbWE9c2lnbWEpCmN0cmwgPC0gdHJhaW5Db250cm9sKG1ldGhvZD0iY3YiKQpyZXMuY2FyZXQxIDwtIHRyYWluKFl+LixkYXRhPXRyYWluLG1ldGhvZD0ic3ZtUmFkaWFsIix0ckNvbnRyb2w9Y3RybCx0dW5lR3JpZD1ncixwcm9iLm1vZGVsPVRSVUUpCnJlcy5jYXJldDEKYGBgCgpPbiBwZXV0IMOpZ2FsZW1lbnQgcsOpcMOpdGVyIHBsdXNpZXVycyBmb2lzIGxhIHZhbGlkYXRpb24gY3JvaXPDqWUgcG91ciBzdGFiaWxpc2VyIGxlcyByw6lzdWx0YXRzIChvbiBwYXJhbGzDqWxpc2UgYXZlYyAqKmRvUGFyYWxsZWwqKikgOgoKYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShkb1BhcmFsbGVsKSAjIyBwb3VyIHBhcmFsbMOpbGlzZXIKY2wgPC0gbWFrZVBTT0NLY2x1c3Rlcig0KQpyZWdpc3RlckRvUGFyYWxsZWwoY2wpCnNldC5zZWVkKDEyMzQ1KQpjdHJsIDwtIHRyYWluQ29udHJvbChtZXRob2Q9InJlcGVhdGVkY3YiLG51bWJlcj0xMCxyZXBlYXRzPTUpCnJlcy5jYXJldDIgPC0gdHJhaW4oWX4uLGRhdGE9dHJhaW4sbWV0aG9kPSJzdm1SYWRpYWwiLHRyQ29udHJvbD1jdHJsLHR1bmVHcmlkPWdyLHByb2IubW9kZWw9VFJVRSkKb24uZXhpdChzdG9wQ2x1c3RlcihjbCkpCnJlcy5jYXJldDIKYGBgCgo4LiBDb21wYXJlciBsYSBzdm0gc8OpbGVjdGlvbm7DqWUgw6AgbGEgc3ZtIGxpbsOpYWlyZSDDoCBsJ2FpZGUgZGUgbGEgY291cmJlIFJPQyBldCBkZSBsJ2VycmV1ciBkZSBjbGFzc2lmaWNhdGlvbi4gT24gcG91cnJhIGNyw6llciB1bmUgdGFibGUgcXVpIGNvbnRpZW50IGxlcyBzY29yZXMgZXQgbGVzIGxhYmVscyBvYnNlcnbDqXMgZGVzIGluZGl2aWR1cyBkZSBsJ8OpY2hhbnRpbGxvbiB0ZXN0LiBPbiBwb3VycmEgw6lnYWxlbWVudCBham91dGVyIHVuZSBzdm0gcG9seW5vbWlhbGUuCgpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpzY2FsZSA8LSAxCkMgPC0gYygxLDEwLDEwMCkKZGVncmVlIDwtIGMoMTozKQpnciA8LSBleHBhbmQuZ3JpZChDPUMsc2NhbGU9c2NhbGUsZGVncmVlPWRlZ3JlZSkKY3RybCA8LSB0cmFpbkNvbnRyb2wobWV0aG9kPSJyZXBlYXRlZGN2IixudW1iZXI9MTAscmVwZWF0cz01KQpjbCA8LSBtYWtlUFNPQ0tjbHVzdGVyKDQpCnJlZ2lzdGVyRG9QYXJhbGxlbChjbCkKc2V0LnNlZWQoMTIzNDUpCnJlcy5jYXJldDMgPC0gdHJhaW4oWX4uLGRhdGE9dHJhaW4sbWV0aG9kPSJzdm1Qb2x5Iix0ckNvbnRyb2w9Y3RybCx0dW5lR3JpZD1ncixwcm9iLm1vZGVsPVRSVUUpCm9uLmV4aXQoc3RvcENsdXN0ZXIoY2wpKQoKYGBgCgoKYGBge3J9Cm1vZC5zdm0wIDwtIHN2bShZfi4sZGF0YT10cmFpbixrZXJuZWw9ImxpbmVhciIsY29zdD0xLHByb2JhYmlsaXR5PVRSVUUpCnByZXYubGluIDwtIHByZWRpY3QobW9kLnN2bTAsbmV3ZGF0YT10ZXN0LHByb2JhYmlsaXR5PVRSVUUpCnByZXYubGluIDwtIGF0dHIocHJldi5saW4sICJwcm9iYWJpbGl0aWVzIilbLDJdCnByZXYucmFkIDwtIHByZWRpY3QocmVzLmNhcmV0MixuZXdkYXRhPXRlc3QsdHlwZT0icHJvYiIpWywyXQpwcmV2LnBvbHkgPC0gcHJlZGljdChyZXMuY2FyZXQzLG5ld2RhdGE9dGVzdCx0eXBlPSJwcm9iIilbLDJdCmBgYAoKYGBge3J9CnNjb3JlIDwtIGRhdGEuZnJhbWUobGluPXByZXYubGluLHJhZD1wcmV2LnJhZCxwb2x5PXByZXYucG9seSxvYnM9YXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIodGVzdCRZKSkpICU+JSAKICBnYXRoZXIoa2V5PSJNZXRob2QiLHZhbHVlPSJzY29yZSIsLW9icykgJT4lIAogIG11dGF0ZShwcmV2PXJlY29kZShhcy5jaGFyYWN0ZXIoc2NvcmU+MC41KSwiVFJVRSI9MSwiRkFMU0UiPTApKQpgYGAKCgoqIGBDb3VyYmUgUk9DYAoKYGBge3J9CmxpYnJhcnkocGxvdFJPQykKZ2dwbG90KHNjb3JlKSthZXMoZD1vYnMsbT1zY29yZSxjb2xvcj1NZXRob2QpK2dlb21fcm9jKCkrdGhlbWVfY2xhc3NpYygpCmBgYAoKKiBgQVVDYAoKYGBge3J9CnNjb3JlICU+JSBncm91cF9ieShNZXRob2QpICU+JSBzdW1tYXJpemUoQVVDPXBST0M6OmF1YyhvYnMsc2NvcmUpKSAlPiUgYXJyYW5nZShkZXNjKEFVQykpCmBgYAoKKiBgRXJyZXVyIGRlIGNsYXNzaWZpY2F0aW9uYAoKYGBge3J9CnNjb3JlICU+JSBncm91cF9ieShNZXRob2QpICU+JSBzdW1tYXJpemUoRXJyZXVyPW1lYW4ocHJldiE9b2JzKSkgJT4lIGFycmFuZ2UoRXJyZXVyKQpgYGAKCgoKOS4gQSBsJ2FpZGUgZGUgKipjYXJldCoqLCBzw6lsZWN0aW9ubmVyIGxlcyBwYXJhbcOodHJlcyBkZSBsYSBzdm0gZW4gb3B0aW1pc2FudCBsJ0FVQy4KCk9uIHBldXQgdXRsaXNlciBsJ29wdGlvbiAqKm1ldHJpYyoqIGRlIGxhIGZvbmN0aW9uICoqdHJhaW4qKiA6CgoKYGBge3J9CmN0cmwgPC0gdHJhaW5Db250cm9sKG1ldGhvZD0iY3YiLGNsYXNzUHJvYnMgPSBUUlVFLHN1bW1hcnkgPSB0d29DbGFzc1N1bW1hcnkpCnRyYWluMSA8LSB0cmFpbiAlPiUgbXV0YXRlKFk9ZmN0X3JlY29kZShZLEcwPSIwIixHMT0iMSIpKQpyZXMuY2FyZXQ0IDwtIHRyYWluKFl+LixkYXRhPXRyYWluMSxtZXRob2Q9InN2bVBvbHkiLHRyQ29udHJvbD1jdHJsLHR1bmVHcmlkPWdyLHByb2IubW9kZWw9VFJVRSxtZXRyaWM9IlJPQyIpCnJlcy5jYXJldDQKYGBgCgoKCg==