PARTE 2

1. Framework MLR3

1.1. Introducción

Acorde a Becker y otros (2021), el paquete mlr3 provee un marco de trabajo genérico para varias tareas de aprendizaje automático, inluyendo problemas de clasificación, regresión, análisis de supervivencia y otro tipo de modelos para el lenguaje R. Contrario a librerías de python como scikit-learn, lo que hace este paquete es proveer una interfaz unificada para el entrenamiento, selección de variables, optimización de hiperparámetros y evaluación de modelos de aprendizaje automático que podemos encontrar en varias librerías de R. Para muchos de estos casos, la paralelización de procesos viene nativamente soportada.

Si bien mlr3 provee la funcionalidad base del framework, existen librerías que extienden los procesos disponibles, mejorando la creación de flujos de trabajo (pipelines), preprocesamiento, visualización, etc. Estos se pueden observar a continuación:

Finalmente, los creadores de esta librería mencionan que se siguen varios principios de diseño enlistados a continuación, con la finalidad de mantener uniformidad en el análisis de aprendizaje automático realizado con esta librería.

  • Se prioriza el backend sobre el frontend. Es decir que la gran mayoría de paquetes del ecosistema mlr3 se enfocan en el procesamiento y transformación de datos, estimación de modelos y comparación de resultados, por lo que su objetivo no es ofrecer una interfaz de usuario gráfica, apoyándose para ello de otras librerías como ggplot2.

  • Se utilizan objetos del tipo R6 con la finalidad de lograr un diseño limpio, orientado a objetos, cambiado su estado con cada ejecución que se realice. Los objetos R6 son internamente tratados como ambientes o environments.

  • Dado que librería se enfoca en el aprendizaje automático sobre datos coleccionados en memoria (y no en entornos distribuidos), utiliza la librería data.table para realizar la manipulación de datos de manera rápida y eficiente.

  • Unifica los objetos que se objetan como resultado en objetos data.tables, permitiendo operaciones rápidas de selección y agregación.

  • Utiliza programación defensiva y segura ante errores de tipeo, evitando simplificaciones dentro del código.

  • Es ligero en dependencias.

Una vez que hemos interiorizado estas definiciones, podemos comenzar el análisis de clasificación de noticias falsas en la siguiente subsección.

1.2. Proceso de modelamiento en MLR3

Con el uso de la librería mlr3, a continuación se indica el proceso general de modelamiento utilizado.

En esencia, la librería mlr3 encapsula a los datos de análisis en tasks o tareas. En cada tarea, divide a los datos en conjuntos de datos de entrenamiento y prueba. Dado que el objetivo del aprendizaje automático es construir un modelo que pueda ser extrapolado a nuevos conjuntos de datos, se utilizan los datos de entrenamiento para la estimación de algoritmos, mientras que los datos de prueba se utilizan para evaluar el desempeño de los modelos.

Una vez divididos los datos dentro de la tarea, se entregan los datos de entrenamiento a un learner, el cual es un algoritmo de aprendizaje automático. Este learner construye el modelo y puede ser usado para predecir o para evaluarse a través de distintas métricas.

Adicionalmente podemos aumentar etapas de preprocesamiento entre la etapa de división de los datos y entrenamiento del modelo, o aplicar técnicas de remuestreo para que nuestros modelos sean más robustos.

Sin más preámbulo, manos a la obra, y para ello, cargaremos la librería y los datos que hemos procesado anteriormente.

# Carga de librerías
library(mlr3)
library(mlr3viz)
library(mlr3pipelines)
library(mlr3verse)
library(mlr3tuning)
library(tidyverse)
library(ggpubr)
library(rpart.plot)

# Carga de datos
fake_news_model_df = readRDS("Caso3_NoticiasFalsas/fake_news_model_df.RDS")

2. Modelo de clasificación de noticias falsas

2.1. Definición de una tarea

Para comenzar el análisis, definamos una tarea que pueda ser ejecutada por el framework *mlr3*, eliminando las variables identificadores y de texto que no usaremos para predecir.

# Conversión de variable dependiente a factor y remoción de variables
fake_news_task_df = fake_news_model_df %>% 
  mutate(target_falsa = factor(target_falsa,levels=c(0,1),
                               labels=c("Verídica","Falsa"))) %>% 
  select(-status_id, -screen_name, -text)

# Creación de la tarea
task_fake_news = TaskClassif$new(id = "fake_news", 
                                 backend = fake_news_task_df, 
                                 target = "target_falsa")
print(task_fake_news)
<TaskClassif:fake_news> (13892 x 33)
* Target: target_falsa
* Properties: twoclass
* Features (32):
  - int (21): n_hashtags, n_palabras, n_palabras_ADJ, n_palabras_ADV, n_palabras_DET,
    n_palabras_NOUN, n_palabras_PRON, n_palabras_PROPN, n_palabras_PUNCT,
    n_palabras_VERB, n_palabras_unicas, n_palabras_unicas_ADJ, n_palabras_unicas_ADV,
    n_palabras_unicas_DET, n_palabras_unicas_NOUN, n_palabras_unicas_PRON,
    n_palabras_unicas_PROPN, n_palabras_unicas_PUNCT, n_palabras_unicas_VERB,
    n_signos_admiracion, n_signos_interrogacion
  - dbl (10): anger, anticipation, disgust, fear, joy, negative, positive, sadness,
    surprise, trust
  - fct (1): hora_publicacion

Una vez creada la tarea, podremos llamar a los datos que se incluyen en esta, y otros atributos, como se muestra a continuación.

# Datos
task_fake_news$data()
# Clases en la variable objetivo
task_fake_news$class_names
[1] "Verídica" "Falsa"   
# Variables explicativas
task_fake_news$feature_names
 [1] "anger"                   "anticipation"            "disgust"                
 [4] "fear"                    "hora_publicacion"        "joy"                    
 [7] "n_hashtags"              "n_palabras"              "n_palabras_ADJ"         
[10] "n_palabras_ADV"          "n_palabras_DET"          "n_palabras_NOUN"        
[13] "n_palabras_PRON"         "n_palabras_PROPN"        "n_palabras_PUNCT"       
[16] "n_palabras_VERB"         "n_palabras_unicas"       "n_palabras_unicas_ADJ"  
[19] "n_palabras_unicas_ADV"   "n_palabras_unicas_DET"   "n_palabras_unicas_NOUN" 
[22] "n_palabras_unicas_PRON"  "n_palabras_unicas_PROPN" "n_palabras_unicas_PUNCT"
[25] "n_palabras_unicas_VERB"  "n_signos_admiracion"     "n_signos_interrogacion" 
[28] "negative"                "positive"                "sadness"                
[31] "surprise"                "trust"                  

Así también, gracias a la extensión mlr3viz podemos realizar varios tipos de visualizaciones para la tarea definida.

# Gráfico de la variable target
autoplot(task_fake_news)

2.2. Learners

Una vez que la tarea ha sido definida, deberemos definir el algoritmo que vamos a usar. Para ello, existe en la página ml3learners una lista de los algoritmos que podemos utilizar, entre los cuales podemos encontrar (para llevar a cabo tareas de clasificación):

ID Algoritmo Librería
classif.cv_glmnet Regresión logística penalizada glmnet
classif.glmnet Regresión logística penalizada glmnet
classif.kknn k-vecinos más cercanos kknn
classif.lda Análisis discriminante lineal MASS
classif.log_reg Regresión logística stats
classif.multinom Modelo multinomial lineal logarítimico nnet
classif.naive_bayes Naive Bayes e1071
classif.nnet Redes neuronales de una sola neurona nnet
classif.qda Análisis discrimante cuadrático MASS
classif.ranger Bosques aleatorios ranger
classif.svm Máquinas de soporte vectorial e1071
classif.xgboost Gradient Boosting xgboost

Cualquier de estos algoritmos será estimado en los datos de entrenamiento, es decir sobre un subconjunto de los datos analizados, para ser evaluado en los datos de prueba (o datos fuera de la muestra de entrenamiento), como se muestra en la siguiente imagen.

A través de las siguientes líneas de código también podemos observar los algoritmos existentes y sus hiperparámetros. Cabe notar que los hiperparámetros de un modelo son los valores de las configuraciones utilizadas por el algoritmo durante el proceso de entrenamiento, permitiendo su afinación. Específicamente, estos son valores que generalmente no se obtienen de los datos, por lo que tienen que ser especificados, y su valor óptimo tendrá que ser encontrado a través de métodos de pruba y error, uso de reglas o métodos de optimización de hiperparámetros, los cuales visitaremos más adelante.

# Lista de algoritmos de clasificación
classif_learners = mlr_learners$keys()[startsWith(mlr_learners$keys(),"classif")]
classif_learners
 [1] "classif.cv_glmnet"   "classif.debug"       "classif.featureless" "classif.glmnet"     
 [5] "classif.kknn"        "classif.lda"         "classif.log_reg"     "classif.multinom"   
 [9] "classif.naive_bayes" "classif.nnet"        "classif.qda"         "classif.ranger"     
[13] "classif.rpart"       "classif.svm"         "classif.xgboost"    
# Hiperparámetros
lrn("classif.rpart")$param_set$ids()
 [1] "minsplit"       "minbucket"      "cp"             "maxcompete"     "maxsurrogate"  
 [6] "maxdepth"       "usesurrogate"   "surrogatestyle" "xval"           "keep_model"    

2.3. Entrenamiento de un modelo

Ahora que conocemos cómo definir una tarea y un algoritmo, vamos a entrenar un modelo. Para ello, dividimos primero el conjunto de datos original en dos subconjuntos: entrenamiento y validación. Por lo general se suele tomar el 70% de los datos como muestra de entrenamiento, aunque este criterio dependerá de la situación y el analista.

# Definición de entrenamiento y validación
train_set = sample(task_fake_news$nrow, 0.7 * task_fake_news$nrow)
test_set = setdiff(seq_len(task_fake_news$nrow), train_set)
cat("Dimensión en entrenamiento:",length(train_set),"\n")
Dimensión en entrenamiento: 9724 
cat("Dimensión en testing:",length(test_set),"\n")
Dimensión en testing: 4168 

Una vez divididos los datos, inicializamos y entrenamos el modelo (en este caso, un árbol de decisión) con los parámetros por defecto del algoritmo utilizado.

# Inicialización de learner
example_learner = lrn("classif.rpart")
example_learner$predict_type = "prob"

# Entrenamiento 
example_learner$train(task_fake_news, row_ids = train_set)

# Resultados del modelo
rpart.plot(example_learner$model)
Cannot retrieve the data used to build the model (so cannot determine roundint and is.binary for the variables).
To silence this warning:
    Call rpart.plot with roundint=FALSE,
    or rebuild the rpart model with model=TRUE.

Luego de haber estimado el modelo en los datos de entrenamiento, podemos pasar a realizar las predicciones en el conjunto de datos de test y evaluar allí su rendimiento.

2.4. Predicción

Para realizar la predicción no hace falta nada más que una línea de código.

# Predicción
prediction_lr = example_learner$predict(task_fake_news, row_ids = test_set)
print(prediction_lr)
<PredictionClassif> for 4168 observations:

Una vez que estas predicciones han sido hechas, revisemos su distribución con respecto al valor real de la variable objetivo.

# Gráfico de la predicción
autoplot(prediction_lr)

En este caso, el modelo parece tener un buen desempeño, dado que tiene predicciones bastante similares a la de los datos reales.

2.5. Evaluación del desempeño

Para evaluar el desempeño de la clasificación binaria existen varias medidas, de las cuales, la gran mayoría se obtiene de la matriz de confusión. La lógica de esta matriz se puede visualizar a continuación.

Básicamente, en la matriz de confusión se comparan los valores verdaderos de la variable objetivo contra los valores predichos por el modelo en una tabla de doble entrada. A partir de esta matriz se pueden obtener todas las medidas especificadas anteriormente.

La matriz de confusión, así como sus indicadores, se pueden obtener a través del siguiente código.

# Matriz de confusión
prediction_lr$confusion
          truth
response   Verídica Falsa
  Verídica     2705   677
  Falsa         129   657
# Medidas de desempeño
example_measure = list(msr("classif.acc", id = "accuracy"),
                  msr("classif.auc", id = "auc"),
                  msr("classif.precision", id = "precision"),
                  msr("classif.recall", id = "recall"),
                  msr("classif.sensitivity", id = "sensitivity"),
                  msr("classif.specificity", id = "specificity"),
                  msr("classif.tn", id="true negative"),
                  msr("classif.tp", id="true positive"),
                  msr("classif.fn", id="false negative"),
                  msr("classif.fp", id="false positive"))
sapply(example_measure, function(x)prediction_lr$score(x))
      accuracy            auc      precision         recall    sensitivity    specificity 
     0.8066219      0.7885180      0.7998226      0.9544813      0.9544813      0.4925037 
 true negative  true positive false negative false positive 
   657.0000000   2705.0000000    129.0000000    677.0000000 

Adicional a estos indicadores, es posible graficar la curva ROC (Receiver Operating Characteristics) y calcular el área bajo ella (AUC). La ROC es la representación de la razón o proporción de verdaderos positivos frente a la razón o proporción de falsos positivos, según se varía el umbral de discriminación (valor a partir del cual decidimos que un caso es un positivo, es decir, que una noticia es falsa o no). Por su parte, el AUC puede resumir cómo funciona un modelo clasificador en una sola medida. Conforme más se acerque a 1, mejor será su poder de clasificación, mientras que, conforme se acerca a 0.5, su poder de clasificación será más similar al de un clasificador aleatorio.

# Curvas
autoplot(prediction_lr, type = "roc")

En este caso nuestro modelo alcanza un nivel razonable de ROC, pero podría hacerlo mejor y para ello veremos cómo nos ayuda la optimización de hiperparámetros.

2.6. Optimización de hiperparámetros

Pese a que los hiperparámetros de un modelo no son observados a primera vista durante la inicialización del modelo (en el ejercicio que acabamos de hacer no editamos ningún hiperparámetro), estos juega un rol crucial en el desempeño del algoritmo. Normalmente estos se suelen fijar al iniciar el entrenamiento, pero es posible seleccionar sus valores a través del entrenamiento de varios modelos que nos permitan comprobar el efecto que estos tienen sobre el desempeño del modelo. Para ello se ejecuta un procedimiento conocido como optimización o afinación de hiperparámetros. Este proceso se puede ver de manera gráfica a continuación.

Básicamente lo que hacemos es entrenar varios modelos para un conjunto de hiperparámetros definidos con anterioridad, para luego compararlos y escoger así sus valores ‘’óptimos’’. A continuación resolveremos el problema entre manos a través del uso de un algoritmo de árboles de clasificación, optimizando dos de sus hiperparámetros: el parámetro de complejidad (cp) y el número de observaciones mínimas exigidas en cada nodo (minsplit). Esto lo logramos a través del siguiente código.

# Inicialización del learner
optimizing_learner = lrn("classif.rpart")

# Definición del espacio de búsqueda
search_space = ps(cp = p_dbl(lower = 0.001, upper = 0.1),
                  minsplit = p_int(lower = 20, upper = 100))
search_space
<ParamSet>

Para ejecutar la afinación deberemos definir previamente tres parámetros:

  • El método de remuestro, es decir, ¿seguiremos usando una sola división de entrenamiento y validación o lo haremos sobre varias muestras distintas tomadas aleatoriamente de los datos (validación cruzada)?
  • Una medida de evaluación de desempeño, la cual definirá el mejor modelo.
  • Un criterio de finalización, es decir, ¿detendremos la optimización después de un periodo de tiempo (dado que algunos algoritmos podrían tomar mucho en ejecutarse)? o ¿la detendremos cuando el modelo ya no se pueda mejorar?

Estos criterios se definen como se muestra a continuación.

# Método de evaluación
resamp_method = rsmp("holdout", ratio = 0.7)
measure_method = msr("classif.acc")

# Criterio de finalización
ending_method = trm("stagnation")

Una vez que tenemos estos parámetros definidos, crearemos la instancia de afinación con el siguiente código.

# Instancia de optimización
instance = TuningInstanceSingleCrit$new(task = task_fake_news,
                                        learner = optimizing_learner,
                                        resampling = resamp_method,
                                        measure = measure_method,
                                        search_space = search_space,
                                        terminator = ending_method)
instance
<TuningInstanceSingleCrit>
* State:  Not optimized
* Objective: <ObjectiveTuning:classif.rpart_on_fake_news>
* Search Space:
<ParamSet>
* Terminator: <TerminatorStagnation>
* Terminated: FALSE
* Archive:
<ArchiveTuning>

Finalmente realizaremos el entrenamiento a través de uno de los siguientes métodos existentes dentro de la librería mlr3:

  • Búsqueda cartesiana (se estima un modelo para cada combinación posible de los hiperparámetros especificados)
  • Búsqueda aleatoria (se estima un modelo para cada combinación posible de los hiperparámetros tomados de una distribución de probabilidad especificada)
  • Optimización no lineal (se optimizan los parámetros a través de la búsqueda de un óptimo global o local)

En este caso, lo haremos a través de búsqueda aleatoria.

# Método de búsqueda
tuner = tnr("random_search")

# Optimización
tuner$optimize(instance)
INFO  [14:56:18.276] [bbotk] Starting to optimize 2 parameter(s) with '<OptimizerRandomSearch>' and '<TerminatorStagnation> [iters=10, threshold=0]' 
INFO  [14:56:18.305] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:18.339] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:18.358] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:18.499] [mlr3]  Finished benchmark 
INFO  [14:56:18.602] [bbotk] Result of batch 1: 
INFO  [14:56:18.636] [bbotk]  
INFO  [14:56:18.661] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:18.700] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:18.726] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:18.856] [mlr3]  Finished benchmark 
INFO  [14:56:18.952] [bbotk] Result of batch 2: 
INFO  [14:56:18.986] [bbotk]  
INFO  [14:56:19.012] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:19.058] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:19.087] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:19.228] [mlr3]  Finished benchmark 
INFO  [14:56:19.341] [bbotk] Result of batch 3: 
INFO  [14:56:19.373] [bbotk]  
INFO  [14:56:19.397] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:19.443] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:19.469] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:19.640] [mlr3]  Finished benchmark 
INFO  [14:56:19.738] [bbotk] Result of batch 4: 
INFO  [14:56:19.772] [bbotk]  
INFO  [14:56:19.810] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:19.871] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:19.909] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:20.292] [mlr3]  Finished benchmark 
INFO  [14:56:20.385] [bbotk] Result of batch 5: 
INFO  [14:56:20.456] [bbotk]  
INFO  [14:56:20.486] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:20.553] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:20.577] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:20.769] [mlr3]  Finished benchmark 
INFO  [14:56:20.962] [bbotk] Result of batch 6: 
INFO  [14:56:20.996] [bbotk]  
INFO  [14:56:21.021] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:21.073] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:21.099] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:21.244] [mlr3]  Finished benchmark 
INFO  [14:56:21.355] [bbotk] Result of batch 7: 
INFO  [14:56:21.389] [bbotk]  
INFO  [14:56:21.409] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:21.467] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:21.491] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:21.681] [mlr3]  Finished benchmark 
INFO  [14:56:21.800] [bbotk] Result of batch 8: 
INFO  [14:56:21.834] [bbotk]  
INFO  [14:56:21.863] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:21.909] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:21.938] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:22.096] [mlr3]  Finished benchmark 
INFO  [14:56:22.188] [bbotk] Result of batch 9: 
INFO  [14:56:22.224] [bbotk]  
INFO  [14:56:22.248] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:22.307] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:22.336] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:22.506] [mlr3]  Finished benchmark 
INFO  [14:56:22.618] [bbotk] Result of batch 10: 
INFO  [14:56:22.653] [bbotk]  
INFO  [14:56:22.674] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:22.721] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:22.749] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:22.893] [mlr3]  Finished benchmark 
INFO  [14:56:22.987] [bbotk] Result of batch 11: 
INFO  [14:56:23.018] [bbotk]  
INFO  [14:56:23.038] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:23.089] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:23.117] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:23.250] [mlr3]  Finished benchmark 
INFO  [14:56:23.340] [bbotk] Result of batch 12: 
INFO  [14:56:23.377] [bbotk]  
INFO  [14:56:23.399] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:23.467] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:23.504] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:23.692] [mlr3]  Finished benchmark 
INFO  [14:56:23.802] [bbotk] Result of batch 13: 
INFO  [14:56:23.842] [bbotk]  
INFO  [14:56:23.865] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:23.912] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:23.942] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:24.075] [mlr3]  Finished benchmark 
INFO  [14:56:24.183] [bbotk] Result of batch 14: 
INFO  [14:56:24.217] [bbotk]  
INFO  [14:56:24.242] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:24.293] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:24.319] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:24.483] [mlr3]  Finished benchmark 
INFO  [14:56:24.573] [bbotk] Result of batch 15: 
INFO  [14:56:24.612] [bbotk]  
INFO  [14:56:24.634] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:24.682] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:24.716] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:24.919] [mlr3]  Finished benchmark 
INFO  [14:56:25.018] [bbotk] Result of batch 16: 
INFO  [14:56:25.055] [bbotk]  
INFO  [14:56:25.078] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:25.123] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:25.149] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:25.286] [mlr3]  Finished benchmark 
INFO  [14:56:25.400] [bbotk] Result of batch 17: 
INFO  [14:56:25.433] [bbotk]  
INFO  [14:56:25.459] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:25.506] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:25.535] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:25.670] [mlr3]  Finished benchmark 
INFO  [14:56:25.767] [bbotk] Result of batch 18: 
INFO  [14:56:25.808] [bbotk]  
INFO  [14:56:25.833] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:25.882] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:25.914] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:26.063] [mlr3]  Finished benchmark 
INFO  [14:56:26.168] [bbotk] Result of batch 19: 
INFO  [14:56:26.202] [bbotk]  
INFO  [14:56:26.223] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:26.284] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:26.310] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:26.459] [mlr3]  Finished benchmark 
INFO  [14:56:26.581] [bbotk] Result of batch 20: 
INFO  [14:56:26.617] [bbotk]  
INFO  [14:56:26.645] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:26.697] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:26.725] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:26.914] [mlr3]  Finished benchmark 
INFO  [14:56:27.037] [bbotk] Result of batch 21: 
INFO  [14:56:27.089] [bbotk]  
INFO  [14:56:27.107] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:27.146] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:27.170] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:27.350] [mlr3]  Finished benchmark 
INFO  [14:56:27.451] [bbotk] Result of batch 22: 
INFO  [14:56:27.483] [bbotk]  
INFO  [14:56:27.507] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:27.556] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:27.583] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:27.730] [mlr3]  Finished benchmark 
INFO  [14:56:27.846] [bbotk] Result of batch 23: 
INFO  [14:56:27.879] [bbotk]  
INFO  [14:56:27.902] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:27.957] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:27.990] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:28.141] [mlr3]  Finished benchmark 
INFO  [14:56:28.239] [bbotk] Result of batch 24: 
INFO  [14:56:28.273] [bbotk]  
INFO  [14:56:28.299] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:28.350] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:28.376] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:28.533] [mlr3]  Finished benchmark 
INFO  [14:56:28.631] [bbotk] Result of batch 25: 
INFO  [14:56:28.668] [bbotk]  
INFO  [14:56:28.694] [bbotk] Evaluating 1 configuration(s) 
INFO  [14:56:28.741] [mlr3]  Running benchmark with 1 resampling iterations 
INFO  [14:56:28.769] [mlr3]  Applying learner 'classif.rpart' on task 'fake_news' (iter 1/1) 
INFO  [14:56:28.899] [mlr3]  Finished benchmark 
INFO  [14:56:29.008] [bbotk] Result of batch 26: 
INFO  [14:56:29.047] [bbotk]  
INFO  [14:56:29.085] [bbotk] Finished optimizing after 26 evaluation(s) 
INFO  [14:56:29.099] [bbotk] Result: 
INFO  [14:56:29.130] [bbotk]  

Una vez estimado la afinación, podremos ver sus resultados óptimos con el código que sigue.

# Resultado de la optimización
instance$result

Con tales resultados, realizamos la reestimación del modelo y observamos sus resultados.

# Inicialización de learner
final_learner = lrn("classif.rpart", cp = instance$result$cp, minsplit = instance$result$minsplit)
final_learner$predict_type = "prob"

# Entrenamiento 
final_learner$train(task_fake_news, row_ids = train_set)

# Resultados del modelo
rpart.plot(final_learner$model)
Cannot retrieve the data used to build the model (so cannot determine roundint and is.binary for the variables).
To silence this warning:
    Call rpart.plot with roundint=FALSE,
    or rebuild the rpart model with model=TRUE.

A la par, evaluamos su desempeño:

# Predicción
prediction_fn = final_learner$predict(task_fake_news, row_ids = test_set)
final_measure = list(msr("classif.acc", id = "accuracy"),
                  msr("classif.auc", id = "auc"),
                  msr("classif.precision", id = "precision"),
                  msr("classif.recall", id = "recall"),
                  msr("classif.sensitivity", id = "sensitivity"),
                  msr("classif.specificity", id = "specificity"),
                  msr("classif.tn", id="true negative"),
                  msr("classif.tp", id="true positive"),
                  msr("classif.fn", id="false negative"),
                  msr("classif.fp", id="false positive"))
sapply(final_measure, function(x)prediction_fn$score(x))
      accuracy            auc      precision         recall    sensitivity    specificity 
     0.8277351      0.8608496      0.8412903      0.9202541      0.9202541      0.6311844 
 true negative  true positive false negative false positive 
   842.0000000   2608.0000000    226.0000000    492.0000000 

Con esta optimización hemos mejorado nuestro modelo. Y en este punto quizás sea prudente comparar una gama más extensa de opciones, pero tal tarea quedará para otro capítulo.

3. Bibliografía

Becker, M. y otros (2021), mlr3 book.
LS0tDQp0aXRsZTogIkFuw6FsaXNpcyBkZSBjb21wb3J0YW1pZW50byBlbiByZWRlcyBzb2NpYWxlcyB1c2FuZG8gUHJvY2VzYW1pZW50byBkZWwgTGVuZ3VhamUgTmF0dXJhbCINCnN1YnRpdGxlOiAnQ2Fww610dWxvIDY6IEFwcmVuZGl6YWplIHN1cGVydmlzYWRvLCBwcm9ibGVtYXMgZGUgY2xhc2lmaWNhY2nDs24gY29uIE5MUCcNCmF1dGhvcjogSHVnbyBQb3JyYXMNCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazoNCiAgICBjc3M6IEVzdGlsb3MuY3NzDQogICAgdG9jOiB0cnVlDQogICAgdG9jX2RlcHRoOiAyDQogICAgdG9jX2Zsb2F0Og0KICAgICAgY29sbGFwc2VkOiB0cnVlDQogICAgICBzbW9vdGhfc2Nyb2xsOiBmYWxzZQ0KYmlibGlvZ3JhcGh5OiBCaWJsaW9ncmFmaWEuYmliDQpjc2w6IGNlcGFsLnhtbA0KLS0tDQoNCiMgKipQQVJURSAyKioNCg0KIyAqKjEuIEZyYW1ld29yayBNTFIzKioNCg0KIyMgKioxLjEuIEludHJvZHVjY2nDs24qKg0KDQpBY29yZGUgYSBAbWxyMjAyMSwgZWwgcGFxdWV0ZSAqbWxyMyogcHJvdmVlIHVuIG1hcmNvIGRlIHRyYWJham8gZ2Vuw6lyaWNvIHBhcmEgdmFyaWFzIHRhcmVhcyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbywgaW5sdXllbmRvIHByb2JsZW1hcyBkZSBjbGFzaWZpY2FjacOzbiwgcmVncmVzacOzbiwgYW7DoWxpc2lzIGRlIHN1cGVydml2ZW5jaWEgeSBvdHJvIHRpcG8gZGUgbW9kZWxvcyBwYXJhIGVsIGxlbmd1YWplIFIuIENvbnRyYXJpbyBhIGxpYnJlcsOtYXMgZGUgcHl0aG9uIGNvbW8gKnNjaWtpdC1sZWFybiosIGxvIHF1ZSBoYWNlIGVzdGUgcGFxdWV0ZSBlcyBwcm92ZWVyIHVuYSBpbnRlcmZheiB1bmlmaWNhZGEgcGFyYSBlbCBlbnRyZW5hbWllbnRvLCBzZWxlY2Npw7NuIGRlIHZhcmlhYmxlcywgb3B0aW1pemFjacOzbiBkZSBoaXBlcnBhcsOhbWV0cm9zIHkgZXZhbHVhY2nDs24gZGUgbW9kZWxvcyBkZSBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyBxdWUgcG9kZW1vcyBlbmNvbnRyYXIgZW4gdmFyaWFzIGxpYnJlcsOtYXMgZGUgUi4gUGFyYSBtdWNob3MgZGUgZXN0b3MgY2Fzb3MsIGxhICoqcGFyYWxlbGl6YWNpw7NuKiogZGUgcHJvY2Vzb3MgdmllbmUgbmF0aXZhbWVudGUgc29wb3J0YWRhLg0KDQpTaSBiaWVuICptbHIzKiBwcm92ZWUgbGEgZnVuY2lvbmFsaWRhZCBiYXNlIGRlbCAqZnJhbWV3b3JrKiwgZXhpc3RlbiBsaWJyZXLDrWFzIHF1ZSBleHRpZW5kZW4gbG9zIHByb2Nlc29zIGRpc3BvbmlibGVzLCBtZWpvcmFuZG8gbGEgY3JlYWNpw7NuIGRlIGZsdWpvcyBkZSB0cmFiYWpvICgqcGlwZWxpbmVzKiksIHByZXByb2Nlc2FtaWVudG8sIHZpc3VhbGl6YWNpw7NuLCBldGMuIEVzdG9zIHNlIHB1ZWRlbiBvYnNlcnZhciBhIGNvbnRpbnVhY2nDs246DQoNCiFbXShmaWdzLzA2X21scjNfZXh0ZW5zaW9ucy5zdmcpDQoNCkZpbmFsbWVudGUsIGxvcyBjcmVhZG9yZXMgZGUgZXN0YSBsaWJyZXLDrWEgbWVuY2lvbmFuIHF1ZSBzZSBzaWd1ZW4gdmFyaW9zIHByaW5jaXBpb3MgZGUgZGlzZcOxbyBlbmxpc3RhZG9zIGEgY29udGludWFjacOzbiwgY29uIGxhIGZpbmFsaWRhZCBkZSBtYW50ZW5lciB1bmlmb3JtaWRhZCBlbiBlbCBhbsOhbGlzaXMgZGUgYXByZW5kaXphamUgYXV0b23DoXRpY28gcmVhbGl6YWRvIGNvbiBlc3RhIGxpYnJlcsOtYS4NCg0KLSAgIFNlIHByaW9yaXphIGVsICpiYWNrZW5kKiBzb2JyZSBlbCAqZnJvbnRlbmQqLiBFcyBkZWNpciBxdWUgbGEgZ3JhbiBtYXlvcsOtYSBkZSBwYXF1ZXRlcyBkZWwgZWNvc2lzdGVtYSAqbWxyMyogc2UgZW5mb2NhbiBlbiBlbCBwcm9jZXNhbWllbnRvIHkgdHJhbnNmb3JtYWNpw7NuIGRlIGRhdG9zLCBlc3RpbWFjacOzbiBkZSBtb2RlbG9zIHkgY29tcGFyYWNpw7NuIGRlIHJlc3VsdGFkb3MsIHBvciBsbyBxdWUgc3Ugb2JqZXRpdm8gKipubyoqIGVzIG9mcmVjZXIgdW5hIGludGVyZmF6IGRlIHVzdWFyaW8gZ3LDoWZpY2EsIGFwb3nDoW5kb3NlIHBhcmEgZWxsbyBkZSBvdHJhcyBsaWJyZXLDrWFzIGNvbW8gKmdncGxvdDIqLg0KDQotICAgU2UgdXRpbGl6YW4gb2JqZXRvcyBkZWwgdGlwbyAqKlI2KiogY29uIGxhIGZpbmFsaWRhZCBkZSBsb2dyYXIgdW4gZGlzZcOxbyBsaW1waW8sIG9yaWVudGFkbyBhIG9iamV0b3MsIGNhbWJpYWRvIHN1IGVzdGFkbyBjb24gY2FkYSBlamVjdWNpw7NuIHF1ZSBzZSByZWFsaWNlLiBMb3Mgb2JqZXRvcyAqKlI2Kiogc29uIGludGVybmFtZW50ZSB0cmF0YWRvcyBjb21vIGFtYmllbnRlcyBvICplbnZpcm9ubWVudHMqLg0KDQotICAgRGFkbyBxdWUgbGlicmVyw61hIHNlIGVuZm9jYSBlbiBlbCBhcHJlbmRpemFqZSBhdXRvbcOhdGljbyBzb2JyZSBkYXRvcyBjb2xlY2Npb25hZG9zIGVuIG1lbW9yaWEgKHkgbm8gZW4gZW50b3Jub3MgZGlzdHJpYnVpZG9zKSwgdXRpbGl6YSBsYSBsaWJyZXLDrWEgKmRhdGEudGFibGUqIHBhcmEgcmVhbGl6YXIgbGEgbWFuaXB1bGFjacOzbiBkZSBkYXRvcyBkZSBtYW5lcmEgcsOhcGlkYSB5IGVmaWNpZW50ZS4NCg0KLSAgIFVuaWZpY2EgbG9zIG9iamV0b3MgcXVlIHNlIG9iamV0YW4gY29tbyByZXN1bHRhZG8gZW4gb2JqZXRvcyAqZGF0YS50YWJsZXMqLCBwZXJtaXRpZW5kbyBvcGVyYWNpb25lcyByw6FwaWRhcyBkZSBzZWxlY2Npw7NuIHkgYWdyZWdhY2nDs24uDQoNCi0gICBVdGlsaXphIHByb2dyYW1hY2nDs24gZGVmZW5zaXZhIHkgc2VndXJhIGFudGUgZXJyb3JlcyBkZSB0aXBlbywgZXZpdGFuZG8gc2ltcGxpZmljYWNpb25lcyBkZW50cm8gZGVsIGPDs2RpZ28uDQoNCi0gICBFcyBsaWdlcm8gZW4gZGVwZW5kZW5jaWFzLg0KDQpVbmEgdmV6IHF1ZSBoZW1vcyBpbnRlcmlvcml6YWRvIGVzdGFzIGRlZmluaWNpb25lcywgcG9kZW1vcyBjb21lbnphciBlbCBhbsOhbGlzaXMgZGUgY2xhc2lmaWNhY2nDs24gZGUgbm90aWNpYXMgZmFsc2FzIGVuIGxhIHNpZ3VpZW50ZSBzdWJzZWNjacOzbi4NCg0KIyMgKioxLjIuIFByb2Nlc28gZGUgbW9kZWxhbWllbnRvIGVuIE1MUjMqKg0KDQpDb24gZWwgdXNvIGRlIGxhIGxpYnJlcsOtYSAqbWxyMyosIGEgY29udGludWFjacOzbiBzZSBpbmRpY2EgZWwgcHJvY2VzbyBnZW5lcmFsIGRlIG1vZGVsYW1pZW50byB1dGlsaXphZG8uDQoNCiFbXShmaWdzLzA2X21sX2Fic3RyYWN0aW9uLnN2Zyl7d2lkdGg9IjEwMCUifQ0KDQpFbiBlc2VuY2lhLCBsYSBsaWJyZXLDrWEgKm1scjMqIGVuY2Fwc3VsYSBhIGxvcyBkYXRvcyBkZSBhbsOhbGlzaXMgZW4gKnRhc2tzKiBvIHRhcmVhcy4gRW4gY2FkYSB0YXJlYSwgZGl2aWRlIGEgbG9zIGRhdG9zIGVuIGNvbmp1bnRvcyBkZSBkYXRvcyBkZSBlbnRyZW5hbWllbnRvIHkgcHJ1ZWJhLiBEYWRvIHF1ZSBlbCBvYmpldGl2byBkZWwgYXByZW5kaXphamUgYXV0b23DoXRpY28gZXMgY29uc3RydWlyIHVuIG1vZGVsbyBxdWUgcHVlZGEgc2VyIGV4dHJhcG9sYWRvIGEgbnVldm9zIGNvbmp1bnRvcyBkZSBkYXRvcywgc2UgdXRpbGl6YW4gbG9zIGRhdG9zIGRlIGVudHJlbmFtaWVudG8gcGFyYSBsYSBlc3RpbWFjacOzbiBkZSBhbGdvcml0bW9zLCBtaWVudHJhcyBxdWUgbG9zIGRhdG9zIGRlIHBydWViYSBzZSB1dGlsaXphbiBwYXJhIGV2YWx1YXIgZWwgZGVzZW1wZcOxbyBkZSBsb3MgbW9kZWxvcy4NCg0KVW5hIHZleiBkaXZpZGlkb3MgbG9zIGRhdG9zIGRlbnRybyBkZSBsYSB0YXJlYSwgc2UgZW50cmVnYW4gbG9zIGRhdG9zIGRlIGVudHJlbmFtaWVudG8gYSB1biAqbGVhcm5lciosIGVsIGN1YWwgZXMgdW4gYWxnb3JpdG1vIGRlIGFwcmVuZGl6YWplIGF1dG9tw6F0aWNvLiBFc3RlICpsZWFybmVyKiBjb25zdHJ1eWUgZWwgbW9kZWxvIHkgcHVlZGUgc2VyIHVzYWRvIHBhcmEgcHJlZGVjaXIgbyBwYXJhIGV2YWx1YXJzZSBhIHRyYXbDqXMgZGUgZGlzdGludGFzIG3DqXRyaWNhcy4NCg0KQWRpY2lvbmFsbWVudGUgcG9kZW1vcyBhdW1lbnRhciBldGFwYXMgZGUgcHJlcHJvY2VzYW1pZW50byBlbnRyZSBsYSBldGFwYSBkZSBkaXZpc2nDs24gZGUgbG9zIGRhdG9zIHkgZW50cmVuYW1pZW50byBkZWwgbW9kZWxvLCBvIGFwbGljYXIgdMOpY25pY2FzIGRlIHJlbXVlc3RyZW8gcGFyYSBxdWUgbnVlc3Ryb3MgbW9kZWxvcyBzZWFuIG3DoXMgcm9idXN0b3MuDQoNClNpbiBtw6FzIHByZcOhbWJ1bG8sIG1hbm9zIGEgbGEgb2JyYSwgeSBwYXJhIGVsbG8sIGNhcmdhcmVtb3MgbGEgbGlicmVyw61hIHkgbG9zIGRhdG9zIHF1ZSBoZW1vcyBwcm9jZXNhZG8gYW50ZXJpb3JtZW50ZS4NCg0KYGBge3IgbWVzc2FnZT1GLCB3YXJuaW5nPUZ9DQojIENhcmdhIGRlIGxpYnJlcsOtYXMNCmxpYnJhcnkobWxyMykNCmxpYnJhcnkobWxyM3ZpeikNCmxpYnJhcnkobWxyM3BpcGVsaW5lcykNCmxpYnJhcnkobWxyM3ZlcnNlKQ0KbGlicmFyeShtbHIzdHVuaW5nKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KGdncHVicikNCmxpYnJhcnkocnBhcnQucGxvdCkNCg0KIyBDYXJnYSBkZSBkYXRvcw0KZmFrZV9uZXdzX21vZGVsX2RmID0gcmVhZFJEUygiQ2FzbzNfTm90aWNpYXNGYWxzYXMvZmFrZV9uZXdzX21vZGVsX2RmLlJEUyIpDQpgYGANCg0KIyAqKjIuIE1vZGVsbyBkZSBjbGFzaWZpY2FjacOzbiBkZSBub3RpY2lhcyBmYWxzYXMqKg0KDQojIyAqKjIuMS4gRGVmaW5pY2nDs24gZGUgdW5hIHRhcmVhKioNCg0KUGFyYSBjb21lbnphciBlbCBhbsOhbGlzaXMsIGRlZmluYW1vcyB1bmEgdGFyZWEgcXVlIHB1ZWRhIHNlciBlamVjdXRhZGEgcG9yIGVsIGZyYW1ld29yayBcKm1scjNcKiwgZWxpbWluYW5kbyBsYXMgdmFyaWFibGVzIGlkZW50aWZpY2Fkb3JlcyB5IGRlIHRleHRvIHF1ZSBubyB1c2FyZW1vcyBwYXJhIHByZWRlY2lyLg0KDQpgYGB7cn0NCiMgQ29udmVyc2nDs24gZGUgdmFyaWFibGUgZGVwZW5kaWVudGUgYSBmYWN0b3IgeSByZW1vY2nDs24gZGUgdmFyaWFibGVzDQpmYWtlX25ld3NfdGFza19kZiA9IGZha2VfbmV3c19tb2RlbF9kZiAlPiUgDQogIG11dGF0ZSh0YXJnZXRfZmFsc2EgPSBmYWN0b3IodGFyZ2V0X2ZhbHNhLGxldmVscz1jKDAsMSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGFiZWxzPWMoIlZlcsOtZGljYSIsIkZhbHNhIikpKSAlPiUgDQogIHNlbGVjdCgtc3RhdHVzX2lkLCAtc2NyZWVuX25hbWUsIC10ZXh0KQ0KDQojIENyZWFjacOzbiBkZSBsYSB0YXJlYQ0KdGFza19mYWtlX25ld3MgPSBUYXNrQ2xhc3NpZiRuZXcoaWQgPSAiZmFrZV9uZXdzIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYWNrZW5kID0gZmFrZV9uZXdzX3Rhc2tfZGYsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdGFyZ2V0ID0gInRhcmdldF9mYWxzYSIpDQpwcmludCh0YXNrX2Zha2VfbmV3cykNCmBgYA0KDQpVbmEgdmV6IGNyZWFkYSBsYSB0YXJlYSwgcG9kcmVtb3MgbGxhbWFyIGEgbG9zIGRhdG9zIHF1ZSBzZSBpbmNsdXllbiBlbiBlc3RhLCB5IG90cm9zIGF0cmlidXRvcywgY29tbyBzZSBtdWVzdHJhIGEgY29udGludWFjacOzbi4NCg0KYGBge3J9DQojIERhdG9zDQp0YXNrX2Zha2VfbmV3cyRkYXRhKCkNCmBgYA0KDQpgYGB7cn0NCiMgQ2xhc2VzIGVuIGxhIHZhcmlhYmxlIG9iamV0aXZvDQp0YXNrX2Zha2VfbmV3cyRjbGFzc19uYW1lcw0KYGBgDQoNCmBgYHtyfQ0KIyBWYXJpYWJsZXMgZXhwbGljYXRpdmFzDQp0YXNrX2Zha2VfbmV3cyRmZWF0dXJlX25hbWVzDQpgYGANCg0KQXPDrSB0YW1iacOpbiwgZ3JhY2lhcyBhIGxhIGV4dGVuc2nDs24gKm1scjN2aXoqIHBvZGVtb3MgcmVhbGl6YXIgdmFyaW9zIHRpcG9zIGRlIHZpc3VhbGl6YWNpb25lcyBwYXJhIGxhIHRhcmVhIGRlZmluaWRhLg0KDQpgYGB7cn0NCiMgR3LDoWZpY28gZGUgbGEgdmFyaWFibGUgdGFyZ2V0DQphdXRvcGxvdCh0YXNrX2Zha2VfbmV3cykNCmBgYA0KDQojIyAqKjIuMi4gTGVhcm5lcnMqKg0KDQpVbmEgdmV6IHF1ZSBsYSB0YXJlYSBoYSBzaWRvIGRlZmluaWRhLCBkZWJlcmVtb3MgZGVmaW5pciBlbCBhbGdvcml0bW8gcXVlIHZhbW9zIGEgdXNhci4gUGFyYSBlbGxvLCBleGlzdGUgZW4gbGEgcMOhZ2luYSBbbWwzbGVhcm5lcnNdKGh0dHBzOi8vbWxyM2xlYXJuZXJzLm1sci1vcmcuY29tL2luZGV4Lmh0bWwpIHVuYSBsaXN0YSBkZSBsb3MgYWxnb3JpdG1vcyBxdWUgcG9kZW1vcyB1dGlsaXphciwgZW50cmUgbG9zIGN1YWxlcyBwb2RlbW9zIGVuY29udHJhciAocGFyYSBsbGV2YXIgYSBjYWJvIHRhcmVhcyBkZSBjbGFzaWZpY2FjacOzbik6DQoNCnwgSUQgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB8IEFsZ29yaXRtbyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwgTGlicmVyw61hICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHwNCnw6LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS18Oi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLXw6LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tfA0KfCBbY2xhc3NpZi5jdl9nbG1uZXRdKGh0dHBzOi8vbWxyM2xlYXJuZXJzLm1sci1vcmcuY29tL3JlZmVyZW5jZS9tbHJfbGVhcm5lcnNfY2xhc3NpZi5jdl9nbG1uZXQuaHRtbCkgICAgIHwgUmVncmVzacOzbiBsb2fDrXN0aWNhIHBlbmFsaXphZGEgICAgICAgICB8IFtnbG1uZXRdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9Z2xtbmV0KSAgIHwNCnwgW2NsYXNzaWYuZ2xtbmV0XShodHRwczovL21scjNsZWFybmVycy5tbHItb3JnLmNvbS9yZWZlcmVuY2UvbWxyX2xlYXJuZXJzX2NsYXNzaWYuZ2xtbmV0Lmh0bWwpICAgICAgICAgICB8IFJlZ3Jlc2nDs24gbG9nw61zdGljYSBwZW5hbGl6YWRhICAgICAgICAgfCBbZ2xtbmV0XShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPWdsbW5ldCkgICB8DQp8IFtjbGFzc2lmLmtrbm5dKGh0dHBzOi8vbWxyM2xlYXJuZXJzLm1sci1vcmcuY29tL3JlZmVyZW5jZS9tbHJfbGVhcm5lcnNfY2xhc3NpZi5ra25uLmh0bWwpICAgICAgICAgICAgICAgfCBrLXZlY2lub3MgbcOhcyBjZXJjYW5vcyAgICAgICAgICAgICAgICAgfCBba2tubl0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT1ra25uKSAgICAgICB8DQp8IFtjbGFzc2lmLmxkYV0oaHR0cHM6Ly9tbHIzbGVhcm5lcnMubWxyLW9yZy5jb20vcmVmZXJlbmNlL21scl9sZWFybmVyc19jbGFzc2lmLmxkYS5odG1sKSAgICAgICAgICAgICAgICAgfCBBbsOhbGlzaXMgZGlzY3JpbWluYW50ZSBsaW5lYWwgICAgICAgICAgfCBbTUFTU10oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT1NQVNTKSAgICAgICB8DQp8IFtjbGFzc2lmLmxvZ19yZWddKGh0dHBzOi8vbWxyM2xlYXJuZXJzLm1sci1vcmcuY29tL3JlZmVyZW5jZS9tbHJfbGVhcm5lcnNfY2xhc3NpZi5sb2dfcmVnLmh0bWwpICAgICAgICAgfCBSZWdyZXNpw7NuIGxvZ8Otc3RpY2EgICAgICAgICAgICAgICAgICAgIHwgc3RhdHMgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgfA0KfCBbY2xhc3NpZi5tdWx0aW5vbV0oaHR0cHM6Ly9tbHIzbGVhcm5lcnMubWxyLW9yZy5jb20vcmVmZXJlbmNlL21scl9sZWFybmVyc19jbGFzc2lmLm11bHRpbm9tLmh0bWwpICAgICAgIHwgTW9kZWxvIG11bHRpbm9taWFsIGxpbmVhbCBsb2dhcsOtdGltaWNvIHwgW25uZXRdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9bm5ldCkgICAgICAgfA0KfCBbY2xhc3NpZi5uYWl2ZV9iYXllc10oaHR0cHM6Ly9tbHIzbGVhcm5lcnMubWxyLW9yZy5jb20vcmVmZXJlbmNlL21scl9sZWFybmVyc19jbGFzc2lmLm5haXZlX2JheWVzLmh0bWwpIHwgTmFpdmUgQmF5ZXMgICAgICAgICAgICAgICAgICAgICAgICAgICAgfCBbZTEwNzFdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9ZTEwNzEpICAgICB8DQp8IFtjbGFzc2lmLm5uZXRdKGh0dHBzOi8vbWxyM2xlYXJuZXJzLm1sci1vcmcuY29tL3JlZmVyZW5jZS9tbHJfbGVhcm5lcnNfY2xhc3NpZi5ubmV0Lmh0bWwpICAgICAgICAgICAgICAgfCBSZWRlcyBuZXVyb25hbGVzIGRlIHVuYSBzb2xhIG5ldXJvbmEgICB8IFtubmV0XShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy9wYWNrYWdlPW5uZXQpICAgICAgIHwNCnwgW2NsYXNzaWYucWRhXShodHRwczovL21scjNsZWFybmVycy5tbHItb3JnLmNvbS9yZWZlcmVuY2UvbWxyX2xlYXJuZXJzX2NsYXNzaWYucWRhLmh0bWwpICAgICAgICAgICAgICAgICB8IEFuw6FsaXNpcyBkaXNjcmltYW50ZSBjdWFkcsOhdGljbyAgICAgICAgfCBbTUFTU10oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT1NQVNTKSAgICAgICB8DQp8IFtjbGFzc2lmLnJhbmdlcl0oaHR0cHM6Ly9tbHIzbGVhcm5lcnMubWxyLW9yZy5jb20vcmVmZXJlbmNlL21scl9sZWFybmVyc19jbGFzc2lmLnJhbmdlci5odG1sKSAgICAgICAgICAgfCBCb3NxdWVzIGFsZWF0b3Jpb3MgICAgICAgICAgICAgICAgICAgICB8IFtyYW5nZXJdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9cmFuZ2VyKSAgIHwNCnwgW2NsYXNzaWYuc3ZtXShodHRwczovL21scjNsZWFybmVycy5tbHItb3JnLmNvbS9yZWZlcmVuY2UvbWxyX2xlYXJuZXJzX2NsYXNzaWYuc3ZtLmh0bWwpICAgICAgICAgICAgICAgICB8IE3DoXF1aW5hcyBkZSBzb3BvcnRlIHZlY3RvcmlhbCAgICAgICAgICB8IFtlMTA3MV0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvcGFja2FnZT1lMTA3MSkgICAgIHwNCnwgW2NsYXNzaWYueGdib29zdF0oaHR0cHM6Ly9tbHIzbGVhcm5lcnMubWxyLW9yZy5jb20vcmVmZXJlbmNlL21scl9sZWFybmVyc19jbGFzc2lmLnhnYm9vc3QuaHRtbCkgICAgICAgICB8IEdyYWRpZW50IEJvb3N0aW5nICAgICAgICAgICAgICAgICAgICAgIHwgW3hnYm9vc3RdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3BhY2thZ2U9eGdib29zdCkgfA0KDQpDdWFscXVpZXIgZGUgZXN0b3MgYWxnb3JpdG1vcyBzZXLDoSBlc3RpbWFkbyBlbiBsb3MgZGF0b3MgZGUgZW50cmVuYW1pZW50bywgZXMgZGVjaXIgc29icmUgdW4gc3ViY29uanVudG8gZGUgbG9zIGRhdG9zIGFuYWxpemFkb3MsIHBhcmEgc2VyIGV2YWx1YWRvIGVuIGxvcyBkYXRvcyBkZSBwcnVlYmEgKG8gZGF0b3MgZnVlcmEgZGUgbGEgbXVlc3RyYSBkZSBlbnRyZW5hbWllbnRvKSwgY29tbyBzZSBtdWVzdHJhIGVuIGxhIHNpZ3VpZW50ZSBpbWFnZW4uDQoNCiFbXShmaWdzLzA2X2xlYXJuZXIuc3ZnKXt3aWR0aD0iMTAwJSJ9DQoNCkEgdHJhdsOpcyBkZSBsYXMgc2lndWllbnRlcyBsw61uZWFzIGRlIGPDs2RpZ28gdGFtYmnDqW4gcG9kZW1vcyBvYnNlcnZhciBsb3MgYWxnb3JpdG1vcyBleGlzdGVudGVzIHkgc3VzIGhpcGVycGFyw6FtZXRyb3MuIENhYmUgbm90YXIgcXVlIGxvcyBoaXBlcnBhcsOhbWV0cm9zIGRlIHVuIG1vZGVsbyBzb24gbG9zIHZhbG9yZXMgZGUgbGFzIGNvbmZpZ3VyYWNpb25lcyB1dGlsaXphZGFzIHBvciBlbCBhbGdvcml0bW8gZHVyYW50ZSBlbCBwcm9jZXNvIGRlIGVudHJlbmFtaWVudG8sIHBlcm1pdGllbmRvIHN1IGFmaW5hY2nDs24uIEVzcGVjw61maWNhbWVudGUsIGVzdG9zIHNvbiB2YWxvcmVzIHF1ZSBnZW5lcmFsbWVudGUgbm8gc2Ugb2J0aWVuZW4gZGUgbG9zIGRhdG9zLCBwb3IgbG8gcXVlIHRpZW5lbiBxdWUgc2VyIGVzcGVjaWZpY2Fkb3MsIHkgc3UgdmFsb3Igw7NwdGltbyB0ZW5kcsOhIHF1ZSBzZXIgZW5jb250cmFkbyBhIHRyYXbDqXMgZGUgbcOpdG9kb3MgZGUgcHJ1YmEgeSBlcnJvciwgdXNvIGRlIHJlZ2xhcyBvIG3DqXRvZG9zIGRlIG9wdGltaXphY2nDs24gZGUgaGlwZXJwYXLDoW1ldHJvcywgbG9zIGN1YWxlcyB2aXNpdGFyZW1vcyBtw6FzIGFkZWxhbnRlLg0KDQpgYGB7cn0NCiMgTGlzdGEgZGUgYWxnb3JpdG1vcyBkZSBjbGFzaWZpY2FjacOzbg0KY2xhc3NpZl9sZWFybmVycyA9IG1scl9sZWFybmVycyRrZXlzKClbc3RhcnRzV2l0aChtbHJfbGVhcm5lcnMka2V5cygpLCJjbGFzc2lmIildDQpjbGFzc2lmX2xlYXJuZXJzDQpgYGANCg0KYGBge3J9DQojIEhpcGVycGFyw6FtZXRyb3MNCmxybigiY2xhc3NpZi5ycGFydCIpJHBhcmFtX3NldCRpZHMoKQ0KYGBgDQoNCiMjICoqMi4zLiBFbnRyZW5hbWllbnRvIGRlIHVuIG1vZGVsbyoqDQoNCkFob3JhIHF1ZSBjb25vY2Vtb3MgY8OzbW8gZGVmaW5pciB1bmEgdGFyZWEgeSB1biBhbGdvcml0bW8sIHZhbW9zIGEgZW50cmVuYXIgdW4gbW9kZWxvLiBQYXJhIGVsbG8sIGRpdmlkaW1vcyBwcmltZXJvIGVsIGNvbmp1bnRvIGRlIGRhdG9zIG9yaWdpbmFsIGVuIGRvcyBzdWJjb25qdW50b3M6IGVudHJlbmFtaWVudG8geSB2YWxpZGFjacOzbi4gUG9yIGxvIGdlbmVyYWwgc2Ugc3VlbGUgdG9tYXIgZWwgNzAlIGRlIGxvcyBkYXRvcyBjb21vIG11ZXN0cmEgZGUgZW50cmVuYW1pZW50bywgYXVucXVlIGVzdGUgY3JpdGVyaW8gZGVwZW5kZXLDoSBkZSBsYSBzaXR1YWNpw7NuIHkgZWwgYW5hbGlzdGEuDQoNCmBgYHtyfQ0KIyBEZWZpbmljacOzbiBkZSBlbnRyZW5hbWllbnRvIHkgdmFsaWRhY2nDs24NCnRyYWluX3NldCA9IHNhbXBsZSh0YXNrX2Zha2VfbmV3cyRucm93LCAwLjcgKiB0YXNrX2Zha2VfbmV3cyRucm93KQ0KdGVzdF9zZXQgPSBzZXRkaWZmKHNlcV9sZW4odGFza19mYWtlX25ld3MkbnJvdyksIHRyYWluX3NldCkNCmNhdCgiRGltZW5zacOzbiBlbiBlbnRyZW5hbWllbnRvOiIsbGVuZ3RoKHRyYWluX3NldCksIlxuIikNCmNhdCgiRGltZW5zacOzbiBlbiB0ZXN0aW5nOiIsbGVuZ3RoKHRlc3Rfc2V0KSwiXG4iKQ0KYGBgDQoNClVuYSB2ZXogZGl2aWRpZG9zIGxvcyBkYXRvcywgaW5pY2lhbGl6YW1vcyB5IGVudHJlbmFtb3MgZWwgbW9kZWxvIChlbiBlc3RlIGNhc28sIHVuIMOhcmJvbCBkZSBkZWNpc2nDs24pIGNvbiBsb3MgcGFyw6FtZXRyb3MgcG9yIGRlZmVjdG8gZGVsIGFsZ29yaXRtbyB1dGlsaXphZG8uDQoNCmBgYHtyfQ0KIyBJbmljaWFsaXphY2nDs24gZGUgbGVhcm5lcg0KZXhhbXBsZV9sZWFybmVyID0gbHJuKCJjbGFzc2lmLnJwYXJ0IikNCmV4YW1wbGVfbGVhcm5lciRwcmVkaWN0X3R5cGUgPSAicHJvYiINCg0KIyBFbnRyZW5hbWllbnRvIA0KZXhhbXBsZV9sZWFybmVyJHRyYWluKHRhc2tfZmFrZV9uZXdzLCByb3dfaWRzID0gdHJhaW5fc2V0KQ0KDQojIFJlc3VsdGFkb3MgZGVsIG1vZGVsbw0KcnBhcnQucGxvdChleGFtcGxlX2xlYXJuZXIkbW9kZWwpDQpgYGANCg0KTHVlZ28gZGUgaGFiZXIgZXN0aW1hZG8gZWwgbW9kZWxvIGVuIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvLCBwb2RlbW9zIHBhc2FyIGEgcmVhbGl6YXIgbGFzIHByZWRpY2Npb25lcyBlbiBlbCBjb25qdW50byBkZSBkYXRvcyBkZSB0ZXN0IHkgZXZhbHVhciBhbGzDrSBzdSByZW5kaW1pZW50by4NCg0KIyMgKioyLjQuIFByZWRpY2Npw7NuKioNCg0KUGFyYSByZWFsaXphciBsYSBwcmVkaWNjacOzbiBubyBoYWNlIGZhbHRhIG5hZGEgbcOhcyBxdWUgdW5hIGzDrW5lYSBkZSBjw7NkaWdvLg0KDQpgYGB7cn0NCiMgUHJlZGljY2nDs24NCnByZWRpY3Rpb25fbHIgPSBleGFtcGxlX2xlYXJuZXIkcHJlZGljdCh0YXNrX2Zha2VfbmV3cywgcm93X2lkcyA9IHRlc3Rfc2V0KQ0KcHJpbnQocHJlZGljdGlvbl9scikNCmBgYA0KDQpVbmEgdmV6IHF1ZSBlc3RhcyBwcmVkaWNjaW9uZXMgaGFuIHNpZG8gaGVjaGFzLCByZXZpc2Vtb3Mgc3UgZGlzdHJpYnVjacOzbiBjb24gcmVzcGVjdG8gYWwgdmFsb3IgcmVhbCBkZSBsYSB2YXJpYWJsZSBvYmpldGl2by4NCg0KYGBge3J9DQojIEdyw6FmaWNvIGRlIGxhIHByZWRpY2Npw7NuDQphdXRvcGxvdChwcmVkaWN0aW9uX2xyKQ0KYGBgDQpFbiBlc3RlIGNhc28sIGVsIG1vZGVsbyBwYXJlY2UgdGVuZXIgdW4gYnVlbiBkZXNlbXBlw7FvLCBkYWRvIHF1ZSB0aWVuZSBwcmVkaWNjaW9uZXMgYmFzdGFudGUgc2ltaWxhcmVzIGEgbGEgZGUgbG9zIGRhdG9zIHJlYWxlcy4NCg0KIyMgKioyLjUuIEV2YWx1YWNpw7NuIGRlbCBkZXNlbXBlw7FvKioNCg0KUGFyYSBldmFsdWFyIGVsIGRlc2VtcGXDsW8gZGUgbGEgY2xhc2lmaWNhY2nDs24gYmluYXJpYSBleGlzdGVuIHZhcmlhcyBtZWRpZGFzLCBkZSBsYXMgY3VhbGVzLCBsYSBncmFuIG1heW9yw61hIHNlIG9idGllbmUgZGUgbGEgbWF0cml6IGRlIGNvbmZ1c2nDs24uIExhIGzDs2dpY2EgZGUgZXN0YSBtYXRyaXogc2UgcHVlZGUgdmlzdWFsaXphciBhIGNvbnRpbnVhY2nDs24uDQoNCiFbXShmaWdzLzA2X2NvbmZfbWF0cml4LnBuZykNCg0KQsOhc2ljYW1lbnRlLCBlbiBsYSBtYXRyaXogZGUgY29uZnVzacOzbiBzZSBjb21wYXJhbiBsb3MgdmFsb3JlcyB2ZXJkYWRlcm9zIGRlIGxhIHZhcmlhYmxlIG9iamV0aXZvIGNvbnRyYSBsb3MgdmFsb3JlcyBwcmVkaWNob3MgcG9yIGVsIG1vZGVsbyBlbiB1bmEgdGFibGEgZGUgZG9ibGUgZW50cmFkYS4gQSBwYXJ0aXIgZGUgZXN0YSBtYXRyaXogc2UgcHVlZGVuIG9idGVuZXIgdG9kYXMgbGFzIG1lZGlkYXMgZXNwZWNpZmljYWRhcyBhbnRlcmlvcm1lbnRlLg0KDQpMYSBtYXRyaXogZGUgY29uZnVzacOzbiwgYXPDrSBjb21vIHN1cyBpbmRpY2Fkb3Jlcywgc2UgcHVlZGVuIG9idGVuZXIgYSB0cmF2w6lzIGRlbCBzaWd1aWVudGUgY8OzZGlnby4NCg0KYGBge3J9DQojIE1hdHJpeiBkZSBjb25mdXNpw7NuDQpwcmVkaWN0aW9uX2xyJGNvbmZ1c2lvbg0KYGBgDQoNCmBgYHtyfQ0KIyBNZWRpZGFzIGRlIGRlc2VtcGXDsW8NCmV4YW1wbGVfbWVhc3VyZSA9IGxpc3QobXNyKCJjbGFzc2lmLmFjYyIsIGlkID0gImFjY3VyYWN5IiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYuYXVjIiwgaWQgPSAiYXVjIiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYucHJlY2lzaW9uIiwgaWQgPSAicHJlY2lzaW9uIiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYucmVjYWxsIiwgaWQgPSAicmVjYWxsIiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYuc2Vuc2l0aXZpdHkiLCBpZCA9ICJzZW5zaXRpdml0eSIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLnNwZWNpZmljaXR5IiwgaWQgPSAic3BlY2lmaWNpdHkiKSwNCiAgICAgICAgICAgICAgICAgIG1zcigiY2xhc3NpZi50biIsIGlkPSJ0cnVlIG5lZ2F0aXZlIiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYudHAiLCBpZD0idHJ1ZSBwb3NpdGl2ZSIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLmZuIiwgaWQ9ImZhbHNlIG5lZ2F0aXZlIiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYuZnAiLCBpZD0iZmFsc2UgcG9zaXRpdmUiKSkNCnNhcHBseShleGFtcGxlX21lYXN1cmUsIGZ1bmN0aW9uKHgpcHJlZGljdGlvbl9sciRzY29yZSh4KSkNCmBgYA0KDQoNCkFkaWNpb25hbCBhIGVzdG9zIGluZGljYWRvcmVzLCBlcyBwb3NpYmxlIGdyYWZpY2FyIGxhIGN1cnZhIFJPQyAoUmVjZWl2ZXIgT3BlcmF0aW5nIENoYXJhY3RlcmlzdGljcykgeSBjYWxjdWxhciBlbCDDoXJlYSBiYWpvIGVsbGEgKEFVQykuIExhIFJPQyBlcyBsYSByZXByZXNlbnRhY2nDs24gZGUgbGEgcmF6w7NuIG8gcHJvcG9yY2nDs24gZGUgdmVyZGFkZXJvcyBwb3NpdGl2b3MgZnJlbnRlIGEgbGEgcmF6w7NuIG8gcHJvcG9yY2nDs24gZGUgZmFsc29zIHBvc2l0aXZvcywgc2Vnw7puIHNlIHZhcsOtYSBlbCB1bWJyYWwgZGUgZGlzY3JpbWluYWNpw7NuICh2YWxvciBhIHBhcnRpciBkZWwgY3VhbCBkZWNpZGltb3MgcXVlIHVuIGNhc28gZXMgdW4gcG9zaXRpdm8sIGVzIGRlY2lyLCBxdWUgdW5hIG5vdGljaWEgZXMgZmFsc2EgbyBubykuIFBvciBzdSBwYXJ0ZSwgZWwgQVVDIHB1ZWRlIHJlc3VtaXIgY8OzbW8gZnVuY2lvbmEgdW4gbW9kZWxvIGNsYXNpZmljYWRvciBlbiB1bmEgc29sYSBtZWRpZGEuIENvbmZvcm1lIG3DoXMgc2UgYWNlcnF1ZSBhIDEsIG1lam9yIHNlcsOhIHN1IHBvZGVyIGRlIGNsYXNpZmljYWNpw7NuLCBtaWVudHJhcyBxdWUsIGNvbmZvcm1lIHNlIGFjZXJjYSBhIDAuNSwgc3UgcG9kZXIgZGUgY2xhc2lmaWNhY2nDs24gc2Vyw6EgbcOhcyBzaW1pbGFyIGFsIGRlIHVuIGNsYXNpZmljYWRvciBhbGVhdG9yaW8uDQoNCmBgYHtyfQ0KIyBDdXJ2YXMNCmF1dG9wbG90KHByZWRpY3Rpb25fbHIsIHR5cGUgPSAicm9jIikNCmBgYA0KDQpFbiBlc3RlIGNhc28gbnVlc3RybyBtb2RlbG8gYWxjYW56YSB1biBuaXZlbCByYXpvbmFibGUgZGUgUk9DLCBwZXJvIHBvZHLDrWEgaGFjZXJsbyBtZWpvciB5IHBhcmEgZWxsbyB2ZXJlbW9zIGPDs21vIG5vcyBheXVkYSBsYSBvcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MuDQoNCiMjICoqMi42LiBPcHRpbWl6YWNpw7NuIGRlIGhpcGVycGFyw6FtZXRyb3MqKg0KDQpQZXNlIGEgcXVlIGxvcyBoaXBlcnBhcsOhbWV0cm9zIGRlIHVuIG1vZGVsbyBubyBzb24gb2JzZXJ2YWRvcyBhIHByaW1lcmEgdmlzdGEgZHVyYW50ZSBsYSBpbmljaWFsaXphY2nDs24gZGVsIG1vZGVsbyAoZW4gZWwgZWplcmNpY2lvIHF1ZSBhY2FiYW1vcyBkZSBoYWNlciBubyBlZGl0YW1vcyBuaW5nw7puIGhpcGVycGFyw6FtZXRybyksIGVzdG9zIGp1ZWdhIHVuIHJvbCBjcnVjaWFsIGVuIGVsIGRlc2VtcGXDsW8gZGVsIGFsZ29yaXRtby4gTm9ybWFsbWVudGUgZXN0b3Mgc2Ugc3VlbGVuIGZpamFyIGFsIGluaWNpYXIgZWwgZW50cmVuYW1pZW50bywgcGVybyBlcyBwb3NpYmxlIHNlbGVjY2lvbmFyIHN1cyB2YWxvcmVzIGEgdHJhdsOpcyBkZWwgZW50cmVuYW1pZW50byBkZSB2YXJpb3MgbW9kZWxvcyBxdWUgbm9zIHBlcm1pdGFuIGNvbXByb2JhciBlbCBlZmVjdG8gcXVlIGVzdG9zIHRpZW5lbiBzb2JyZSBlbCBkZXNlbXBlw7FvIGRlbCBtb2RlbG8uIFBhcmEgZWxsbyBzZSBlamVjdXRhIHVuIHByb2NlZGltaWVudG8gY29ub2NpZG8gY29tbyBvcHRpbWl6YWNpw7NuIG8gYWZpbmFjacOzbiBkZSBoaXBlcnBhcsOhbWV0cm9zLiBFc3RlIHByb2Nlc28gc2UgcHVlZGUgdmVyIGRlIG1hbmVyYSBncsOhZmljYSBhIGNvbnRpbnVhY2nDs24uDQoNCiFbXShmaWdzLzA2X3R1bmluZ19wcm9jZXNzLnN2ZykNCg0KQsOhc2ljYW1lbnRlIGxvIHF1ZSBoYWNlbW9zIGVzIGVudHJlbmFyIHZhcmlvcyBtb2RlbG9zIHBhcmEgdW4gY29uanVudG8gZGUgaGlwZXJwYXLDoW1ldHJvcyBkZWZpbmlkb3MgY29uIGFudGVyaW9yaWRhZCwgcGFyYSBsdWVnbyBjb21wYXJhcmxvcyB5IGVzY29nZXIgYXPDrSBzdXMgdmFsb3JlcyAnJ8OzcHRpbW9zJycuIEEgY29udGludWFjacOzbiByZXNvbHZlcmVtb3MgZWwgcHJvYmxlbWEgZW50cmUgbWFub3MgYSB0cmF2w6lzIGRlbCB1c28gZGUgdW4gYWxnb3JpdG1vIGRlIMOhcmJvbGVzIGRlIGNsYXNpZmljYWNpw7NuLCBvcHRpbWl6YW5kbyBkb3MgZGUgc3VzIGhpcGVycGFyw6FtZXRyb3M6IGVsIHBhcsOhbWV0cm8gZGUgY29tcGxlamlkYWQgKGNwKSB5IGVsIG7Dum1lcm8gZGUgb2JzZXJ2YWNpb25lcyBtw61uaW1hcyBleGlnaWRhcyBlbiBjYWRhIG5vZG8gKG1pbnNwbGl0KS4gRXN0byBsbyBsb2dyYW1vcyBhIHRyYXbDqXMgZGVsIHNpZ3VpZW50ZSBjw7NkaWdvLg0KDQpgYGB7cn0NCiMgSW5pY2lhbGl6YWNpw7NuIGRlbCBsZWFybmVyDQpvcHRpbWl6aW5nX2xlYXJuZXIgPSBscm4oImNsYXNzaWYucnBhcnQiKQ0KDQojIERlZmluaWNpw7NuIGRlbCBlc3BhY2lvIGRlIGLDunNxdWVkYQ0Kc2VhcmNoX3NwYWNlID0gcHMoY3AgPSBwX2RibChsb3dlciA9IDAuMDAxLCB1cHBlciA9IDAuMSksDQogICAgICAgICAgICAgICAgICBtaW5zcGxpdCA9IHBfaW50KGxvd2VyID0gMjAsIHVwcGVyID0gMTAwKSkNCnNlYXJjaF9zcGFjZQ0KYGBgDQoNClBhcmEgZWplY3V0YXIgbGEgYWZpbmFjacOzbiBkZWJlcmVtb3MgZGVmaW5pciBwcmV2aWFtZW50ZSB0cmVzIHBhcsOhbWV0cm9zOg0KDQorIEVsIG3DqXRvZG8gZGUgcmVtdWVzdHJvLCBlcyBkZWNpciwgwr9zZWd1aXJlbW9zIHVzYW5kbyB1bmEgc29sYSBkaXZpc2nDs24gZGUgZW50cmVuYW1pZW50byB5IHZhbGlkYWNpw7NuIG8gbG8gaGFyZW1vcyBzb2JyZSB2YXJpYXMgbXVlc3RyYXMgZGlzdGludGFzIHRvbWFkYXMgYWxlYXRvcmlhbWVudGUgZGUgbG9zIGRhdG9zICh2YWxpZGFjacOzbiBjcnV6YWRhKT8NCisgVW5hIG1lZGlkYSBkZSBldmFsdWFjacOzbiBkZSBkZXNlbXBlw7FvLCBsYSBjdWFsIGRlZmluaXLDoSBlbCBtZWpvciBtb2RlbG8uDQorIFVuIGNyaXRlcmlvIGRlIGZpbmFsaXphY2nDs24sIGVzIGRlY2lyLCDCv2RldGVuZHJlbW9zIGxhIG9wdGltaXphY2nDs24gZGVzcHXDqXMgZGUgdW4gcGVyaW9kbyBkZSB0aWVtcG8gKGRhZG8gcXVlIGFsZ3Vub3MgYWxnb3JpdG1vcyBwb2Ryw61hbiB0b21hciBtdWNobyBlbiBlamVjdXRhcnNlKT8gbyDCv2xhIGRldGVuZHJlbW9zIGN1YW5kbyBlbCBtb2RlbG8geWEgbm8gc2UgcHVlZGEgbWVqb3Jhcj8NCg0KRXN0b3MgY3JpdGVyaW9zIHNlIGRlZmluZW4gY29tbyBzZSBtdWVzdHJhIGEgY29udGludWFjacOzbi4NCg0KYGBge3J9DQojIE3DqXRvZG8gZGUgZXZhbHVhY2nDs24NCnJlc2FtcF9tZXRob2QgPSByc21wKCJob2xkb3V0IiwgcmF0aW8gPSAwLjcpDQptZWFzdXJlX21ldGhvZCA9IG1zcigiY2xhc3NpZi5hY2MiKQ0KDQojIENyaXRlcmlvIGRlIGZpbmFsaXphY2nDs24NCmVuZGluZ19tZXRob2QgPSB0cm0oInN0YWduYXRpb24iKQ0KYGBgDQoNClVuYSB2ZXogcXVlIHRlbmVtb3MgZXN0b3MgcGFyw6FtZXRyb3MgZGVmaW5pZG9zLCBjcmVhcmVtb3MgbGEgaW5zdGFuY2lhIGRlIGFmaW5hY2nDs24gY29uIGVsIHNpZ3VpZW50ZSBjw7NkaWdvLg0KDQpgYGB7cn0NCiMgSW5zdGFuY2lhIGRlIG9wdGltaXphY2nDs24NCmluc3RhbmNlID0gVHVuaW5nSW5zdGFuY2VTaW5nbGVDcml0JG5ldyh0YXNrID0gdGFza19mYWtlX25ld3MsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGVhcm5lciA9IG9wdGltaXppbmdfbGVhcm5lciwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXNhbXBsaW5nID0gcmVzYW1wX21ldGhvZCwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBtZWFzdXJlID0gbWVhc3VyZV9tZXRob2QsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VhcmNoX3NwYWNlID0gc2VhcmNoX3NwYWNlLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlcm1pbmF0b3IgPSBlbmRpbmdfbWV0aG9kKQ0KaW5zdGFuY2UNCmBgYA0KDQpGaW5hbG1lbnRlIHJlYWxpemFyZW1vcyBlbCBlbnRyZW5hbWllbnRvIGEgdHJhdsOpcyBkZSB1bm8gZGUgbG9zIHNpZ3VpZW50ZXMgbcOpdG9kb3MgZXhpc3RlbnRlcyBkZW50cm8gZGUgbGEgbGlicmVyw61hICptbHIzKjoNCg0KKyBCw7pzcXVlZGEgY2FydGVzaWFuYSAoc2UgZXN0aW1hIHVuIG1vZGVsbyBwYXJhIGNhZGEgY29tYmluYWNpw7NuIHBvc2libGUgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MgZXNwZWNpZmljYWRvcykNCisgQsO6c3F1ZWRhIGFsZWF0b3JpYSAoc2UgZXN0aW1hIHVuIG1vZGVsbyBwYXJhIGNhZGEgY29tYmluYWNpw7NuIHBvc2libGUgZGUgbG9zIGhpcGVycGFyw6FtZXRyb3MgdG9tYWRvcyBkZSB1bmEgZGlzdHJpYnVjacOzbiBkZSBwcm9iYWJpbGlkYWQgZXNwZWNpZmljYWRhKQ0KKyBPcHRpbWl6YWNpw7NuIG5vIGxpbmVhbCAoc2Ugb3B0aW1pemFuIGxvcyBwYXLDoW1ldHJvcyBhIHRyYXbDqXMgZGUgbGEgYsO6c3F1ZWRhIGRlIHVuIMOzcHRpbW8gZ2xvYmFsIG8gbG9jYWwpDQoNCkVuIGVzdGUgY2FzbywgbG8gaGFyZW1vcyBhIHRyYXbDqXMgZGUgYsO6c3F1ZWRhIGFsZWF0b3JpYS4NCg0KYGBge3J9DQojIE3DqXRvZG8gZGUgYsO6c3F1ZWRhDQp0dW5lciA9IHRucigicmFuZG9tX3NlYXJjaCIpDQoNCiMgT3B0aW1pemFjacOzbg0KdHVuZXIkb3B0aW1pemUoaW5zdGFuY2UpDQpgYGANCg0KVW5hIHZleiBlc3RpbWFkbyBsYSBhZmluYWNpw7NuLCBwb2RyZW1vcyB2ZXIgc3VzIHJlc3VsdGFkb3Mgw7NwdGltb3MgY29uIGVsIGPDs2RpZ28gcXVlIHNpZ3VlLg0KDQpgYGB7cn0NCiMgUmVzdWx0YWRvIGRlIGxhIG9wdGltaXphY2nDs24NCmluc3RhbmNlJHJlc3VsdA0KYGBgDQoNCkNvbiB0YWxlcyByZXN1bHRhZG9zLCByZWFsaXphbW9zIGxhIHJlZXN0aW1hY2nDs24gZGVsIG1vZGVsbyB5IG9ic2VydmFtb3Mgc3VzIHJlc3VsdGFkb3MuDQoNCmBgYHtyfQ0KIyBJbmljaWFsaXphY2nDs24gZGUgbGVhcm5lcg0KZmluYWxfbGVhcm5lciA9IGxybigiY2xhc3NpZi5ycGFydCIsIGNwID0gaW5zdGFuY2UkcmVzdWx0JGNwLCBtaW5zcGxpdCA9IGluc3RhbmNlJHJlc3VsdCRtaW5zcGxpdCkNCmZpbmFsX2xlYXJuZXIkcHJlZGljdF90eXBlID0gInByb2IiDQoNCiMgRW50cmVuYW1pZW50byANCmZpbmFsX2xlYXJuZXIkdHJhaW4odGFza19mYWtlX25ld3MsIHJvd19pZHMgPSB0cmFpbl9zZXQpDQoNCiMgUmVzdWx0YWRvcyBkZWwgbW9kZWxvDQpycGFydC5wbG90KGZpbmFsX2xlYXJuZXIkbW9kZWwpDQpgYGANCg0KQSBsYSBwYXIsIGV2YWx1YW1vcyBzdSBkZXNlbXBlw7FvOg0KDQpgYGB7cn0NCiMgUHJlZGljY2nDs24NCnByZWRpY3Rpb25fZm4gPSBmaW5hbF9sZWFybmVyJHByZWRpY3QodGFza19mYWtlX25ld3MsIHJvd19pZHMgPSB0ZXN0X3NldCkNCmZpbmFsX21lYXN1cmUgPSBsaXN0KG1zcigiY2xhc3NpZi5hY2MiLCBpZCA9ICJhY2N1cmFjeSIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLmF1YyIsIGlkID0gImF1YyIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLnByZWNpc2lvbiIsIGlkID0gInByZWNpc2lvbiIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLnJlY2FsbCIsIGlkID0gInJlY2FsbCIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLnNlbnNpdGl2aXR5IiwgaWQgPSAic2Vuc2l0aXZpdHkiKSwNCiAgICAgICAgICAgICAgICAgIG1zcigiY2xhc3NpZi5zcGVjaWZpY2l0eSIsIGlkID0gInNwZWNpZmljaXR5IiksDQogICAgICAgICAgICAgICAgICBtc3IoImNsYXNzaWYudG4iLCBpZD0idHJ1ZSBuZWdhdGl2ZSIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLnRwIiwgaWQ9InRydWUgcG9zaXRpdmUiKSwNCiAgICAgICAgICAgICAgICAgIG1zcigiY2xhc3NpZi5mbiIsIGlkPSJmYWxzZSBuZWdhdGl2ZSIpLA0KICAgICAgICAgICAgICAgICAgbXNyKCJjbGFzc2lmLmZwIiwgaWQ9ImZhbHNlIHBvc2l0aXZlIikpDQpzYXBwbHkoZmluYWxfbWVhc3VyZSwgZnVuY3Rpb24oeClwcmVkaWN0aW9uX2ZuJHNjb3JlKHgpKQ0KYGBgDQoNCkNvbiBlc3RhIG9wdGltaXphY2nDs24gaGVtb3MgbWVqb3JhZG8gbnVlc3RybyBtb2RlbG8uIFkgZW4gZXN0ZSBwdW50byBxdWl6w6FzIHNlYSBwcnVkZW50ZSBjb21wYXJhciB1bmEgZ2FtYSBtw6FzIGV4dGVuc2EgZGUgb3BjaW9uZXMsIHBlcm8gdGFsIHRhcmVhIHF1ZWRhcsOhIHBhcmEgb3RybyBjYXDDrXR1bG8uDQoNCiMgKiozLiBCaWJsaW9ncmFmw61hKioNCg==