Fully connected layers

red densa

red densa

Vamos a utilizar la librería

instalación

Keras utiliza como backend TensorFlow. Para poner todo en funcionamiento necesitamos instalar ambas cosas. Para eso usamos la función install_keras() que realiza una instalación por default de basada en el CPU.

Si tienen una GPU (procesador gráfico), las redes funcionan mucho más rápido, porque la GPU permite optimizar las operaciones matriciales. Pero necesitan instalar keras para que corra en el lenguaje de la GPU ( CUDA )

# devtools::install_github("rstudio/keras")
library(keras)
# install_keras()
library(tidyverse)
── Attaching packages ──────────────────────────────────────────────────────────────────────────── tidyverse 1.2.1 ──
✔ ggplot2 3.0.0     ✔ purrr   0.2.5
✔ tibble  1.4.2     ✔ dplyr   0.7.7
✔ tidyr   0.8.1     ✔ stringr 1.3.1
✔ readr   1.1.1     ✔ forcats 0.3.0
── Conflicts ─────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
library(knitr)

Mnist

Este es un problema clásico, de juguete, que sirve desde hace muchos años como benchmark para clasificación de imágenes. Tiene 60.000 imágenes, de 28x28, de números escritos a mano.

mnist <- dataset_mnist()
Using TensorFlow backend.
x_train <- mnist$train$x
y_train <- mnist$train$y
x_test <- mnist$test$x
y_test <- mnist$test$y

Veamos la pinta de los datos

datos de entrada

matrix.rotate <- function(img) { 
    t(apply(img, 2, rev))
}
par(mfrow=c(3, 3))
for (idx in 1:9) {
    label <- y_train[idx]
    image(matrix.rotate(x_train[idx,,]), col = grey(level = seq(1, 0, by=-1/255)), axes=F, main=label)
  
}

Pará pará pará… vos me querés decir que ahora cada observación es una matriz de 28x28?

SI

  • En estadística y Machine Learning estamos acostumbrados a pensar cada observación como una fila, con features en las columnas. Si queremos representar una imagen tenemos que aplanarla. Esto significa que tendríamos 28x28=784 features. Probablemente a continuación necesitaríamos una técnica de reducción de la dimensionalidad, y por ahí seguimos…

  • En las redes neuronales esto cambia. Si bien en el modelo de hoy (Fully conected layers) la capa de entrada sigue teniendo 784 nodos, la estructura de la red permite captar muy bien las relaciones no lineales propias de una imagen.

  • Incluso más, cuando veamos las Convolutional Neural Networks, vamos a utilizar imagenes a color, lo que implica 3 matrices para los 3 canales de color (RGB)

¿qué pinta tiene un gráfico desde el punto de vista matricial?

kable(data.frame(x_train[1,,]))
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 3 18 18 18 126 136 175 26 166 255 247 127 0 0 0 0
0 0 0 0 0 0 0 0 30 36 94 154 170 253 253 253 253 253 225 172 253 242 195 64 0 0 0 0
0 0 0 0 0 0 0 49 238 253 253 253 253 253 253 253 253 251 93 82 82 56 39 0 0 0 0 0
0 0 0 0 0 0 0 18 219 253 253 253 253 253 198 182 247 241 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 80 156 107 253 253 205 11 0 43 154 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 14 1 154 253 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 139 253 190 2 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 11 190 253 70 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 35 241 225 160 108 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 81 240 253 253 119 25 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 45 186 253 253 150 27 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 93 252 253 187 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 249 253 249 64 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 46 130 183 253 253 207 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 39 148 229 253 253 253 250 182 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 24 114 221 253 253 253 253 201 78 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 23 66 213 253 253 253 253 198 81 2 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 18 171 219 253 253 253 253 195 80 9 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 55 172 226 253 253 253 253 244 133 11 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 136 253 253 253 212 135 132 16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Cada valor representa a un pixel, y su valor es su representación en la escala de grises (de 0 a 255). Es decir, cuando mayor es el valor, eso es un pixel más oscuro.

El dato esta en un array de 3 dimensiones (imagen,ancho,largo). Como tenemos 60K imagenes, esto tiene la forma de :

dim(x_train)
[1] 60000    28    28

Es decir, nuestros datos ahora conforman un Tensor:

Tensor

Tensor2

Como mencionabamos arriba lo primero es un reshape de los datos:

  • Pasar de estos arrays de 3 dimensiones a 2 dimensiones (como estamos acostumbrados, un vector por observación)
  • Además, necesitamos convertir la escala de los datos de íntegers entre 0 y 255 a numeros floating point entre 0 y 1
# reshape
x_train <- array_reshape(x_train, c(nrow(x_train), 28*28)) #la primera dimensión es tan larga como la cantidad de observaciones, la segunda dimensión es la matriz aplanada (28*28)
x_test <- array_reshape(x_test, c(nrow(x_test), 28*28))
# rescale
x_train <- x_train / 255
x_test <- x_test / 255

obs: esto lo podríamos hacer con dim <-, pero sería menos performante.

datos de salida

y_train %>% head(.)
[1] 5 0 4 1 9 2

Es un vector de integers entre 0-9.

Dada la implementación de las redes, necesitamos pasarlo a one-hot encoding esto se hace con la función to_categorical() de Keras

y_train <- to_categorical(y_train, 10)
y_test <- to_categorical(y_test, 10)

¿qué pinta tiene esto?

y_train %>% head(.)
     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    0    0    0    0    0    1    0    0    0     0
[2,]    1    0    0    0    0    0    0    0    0     0
[3,]    0    0    0    0    1    0    0    0    0     0
[4,]    0    1    0    0    0    0    0    0    0     0
[5,]    0    0    0    0    0    0    0    0    0     1
[6,]    0    0    1    0    0    0    0    0    0     0

Definción del modelo

Para armar el modelo primero definimos el tipo de modelo. Para eso usamos keras_model_sequential() que nos permite simplemente apilar capas de la red.

  • En la primera capa tenemos que aclarar el input_shape. En este caso el unidimensional (lo aplanamos recién), pero podría ser un tensor de cualquier dimensión (!!)
  • Las capas se agregan con pipes %>%
  • La última capa tiene la misma cantidad de unidades que categorías nuestro output. La salida del modelo es un vector que asigna una probabilidad a cada una da las categorías
  • En cada capa tenemos que definir una función de activación
  • Además agregamos una regularización layer_droput(x) que lo que hace es, en cada iteración del ajuste, ignorar el x% de las conexiones. Esto evita el sobreajuste del modelo
model <- keras_model_sequential() 
model %>% 
  layer_dense(units = 256, activation = 'relu', input_shape = c(784)) %>% 
  layer_dropout(rate = 0.4) %>% 
  layer_dense(units = 128, activation = 'relu') %>%
  layer_dropout(rate = 0.3) %>%
  layer_dense(units = 10, activation = 'softmax')

Funciones de activación

Para este modelo utilizamos dos funciones de activación:

  • Rectified Linear Unit: \[f(x)=max(0,x)\]
  • Softmax : \[ f(x)=\frac{e^{x_i}}{\sum_{j=1}^n e^{x_j}}\]

Definidas en código y gráficamente:

relu <- function(x) ifelse(x >= 0, x, 0)
softmax <- function(x) exp(x) / sum(exp(x))
data.frame(x= seq(from=-1, to=1, by=0.1)) %>% 
  mutate(softmax = softmax(x),
         relu = relu(x)) %>% 
  gather(variable,value,2:3) %>% 
ggplot(., aes(x=x, y=value, group=variable, colour=variable))+
  geom_line(size=1) +
  ggtitle("ReLU & Softmax")+
  theme_minimal()

ReLu es la función de activación que más se utiliza en la actualidad.

Si queremos ver un resumen del modelo:

summary(model)

El modelo tiene 235,146 parámetros para optimizar:

En la primera capa oculta tenemos 256 nodos que se conectan con cada nodo de la capa de entrada (784), además de un bias para cada nodo:

784*256+256
[1] 200960

La capa de droput es una regularización y no ajusta ningún parámetro

la capa densa 2 se conecta con los 256 nodos de la primera capa:

128*256+128
[1] 32896

La tercera capa

128*10+10
[1] 1290

Luego necesitamos compilar el modelo indicando la función de loss, qué tipo de optimizador utilizar, y qué métricas nos importan

model %>% compile(
  loss = 'categorical_crossentropy',
  optimizer = optimizer_rmsprop(),
  metrics = c('accuracy')
)

Entrenamiento

Para ajustar el modelo usamos la función fit(), acá necesitamos pasar los siguientes parámetros:

  • El array con los datos de entrenamiento
  • El array con los outputs
  • epochs: Cuantas veces va a recorrer el dataset de entrenamiento
  • batch_size: de a cuantas imagenes va a mirar en cada iteración del backpropagation
  • validation_split: Hacemos un split en train y validation para evaluar las métricas.
fit_history <- model %>% fit(
  x_train, y_train, 
  epochs = 30, batch_size = 128, 
  validation_split = 0.2
)

Mientras entrenamos el modelo, podemos ver la evolución en el gráfico interactivo que se genera en el viewer de Rstudio.

fit() nos devuelve un objeto que incluye las métricas de loss y accuracy

fit_history
Trained on 48,000 samples, validated on 12,000 samples (batch_size=128, epochs=30)
Final epoch (plot to see history):
     acc: 0.9869
    loss: 0.04941
 val_acc: 0.9797
val_loss: 0.1132 

Este objeto lo podemos graficar con plot() y nos devuelve un objeto de ggplot, sobre el que podemos seguir trabajando

plot(fit_history)+
  theme_minimal()+
  labs(title= "Evolución de Loss y Accuracy en train y validation")

Noten que el modelo entrenado, con el que podemos predecir, sigue siendo model.

es importante guardar el modelo luego de entrenar, para poder reutilizarlo

model %>% save_model_hdf5("../Resultados/fc_model.h5")

y para cargarlo

modelo_preentrenado <- load_model_hdf5("../Resultados/fc_model.h5")
modelo_preentrenado
Model
_____________________________________________________________________________________________________________________
Layer (type)                                        Output Shape                                   Param #           
=====================================================================================================================
dense_4 (Dense)                                     (None, 256)                                    200960            
_____________________________________________________________________________________________________________________
dropout_3 (Dropout)                                 (None, 256)                                    0                 
_____________________________________________________________________________________________________________________
dense_5 (Dense)                                     (None, 128)                                    32896             
_____________________________________________________________________________________________________________________
dropout_4 (Dropout)                                 (None, 128)                                    0                 
_____________________________________________________________________________________________________________________
dense_6 (Dense)                                     (None, 10)                                     1290              
=====================================================================================================================
Total params: 235,146
Trainable params: 235,146
Non-trainable params: 0
_____________________________________________________________________________________________________________________

Si queremos evaluar el modelo sobre el conjunto de test (distinto del de validación) podemos usar la función evaluate()

modelo_preentrenado %>% evaluate(x_test, y_test)

   32/10000 [..............................] - ETA: 24s
  608/10000 [>.............................] - ETA: 2s 
 1088/10000 [==>...........................] - ETA: 1s
 1760/10000 [====>.........................] - ETA: 1s
 2464/10000 [======>.......................] - ETA: 0s
 3072/10000 [========>.....................] - ETA: 0s
 3648/10000 [=========>....................] - ETA: 0s
 4320/10000 [===========>..................] - ETA: 0s
 5184/10000 [==============>...............] - ETA: 0s
 5952/10000 [================>.............] - ETA: 0s
 6688/10000 [===================>..........] - ETA: 0s
 7712/10000 [======================>.......] - ETA: 0s
 8832/10000 [=========================>....] - ETA: 0s
 9888/10000 [============================>.] - ETA: 0s
10000/10000 [==============================] - 1s 79us/step
$loss
[1] 0.1080166

$acc
[1] 0.9803

Para obtener las predicciones sobre un nuevo conjunto de datos utilizamos predict_classes()

modelo_preentrenado %>% predict_classes(x_test) %>% head(.)
[1] 7 2 1 0 4 1
LS0tCnRpdGxlOiAiQ2xhc2UgMTIuIFJlZGVzIE5ldXJvbmFsZXMgSV5bRXN0YXMgbm90YXMgZXN0YW4gYmFzYWRhcyBlbiBodHRwczovL3RlbnNvcmZsb3cucnN0dWRpby5jb20va2VyYXMvI3R1dG9yaWFsc10iCmF1dGhvcjogIkRpZWdvIEtvemxvd3NraSB5IEp1YW4gQmFycmlvbGEiCmRhdGU6IDI0LTExLTIwMTgKb3V0cHV0OiAKICBodG1sX25vdGVib29rOiAKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBkZXB0aDogMgotLS0KCmBgYHtyIHBhY2thZ2Vfb3B0aW9ucywgaW5jbHVkZT1GQUxTRX0Ka25pdHI6Om9wdHNfa25pdCRzZXQocHJvZ3Jlc3MgPSBGQUxTRSwgdmVyYm9zZSA9IFRSVUUpCmBgYAoKCiMgRnVsbHkgY29ubmVjdGVkIGxheWVycwoKCiFbcmVkIGRlbnNhXShpbWcvbm4ucG5nKQoKVmFtb3MgYSB1dGlsaXphciBsYSBsaWJyZXLDrWEgCgojIyMjIFtfX0tFUkFTX19dKGh0dHBzOi8va2VyYXMucnN0dWRpby5jb20vKQoKCiMjIGluc3RhbGFjacOzbgoKS2VyYXMgdXRpbGl6YSBjb21vIGJhY2tlbmQgX19UZW5zb3JGbG93X18uIFBhcmEgcG9uZXIgdG9kbyBlbiBmdW5jaW9uYW1pZW50byBuZWNlc2l0YW1vcyBpbnN0YWxhciBhbWJhcyBjb3Nhcy4gUGFyYSBlc28gdXNhbW9zIGxhIGZ1bmNpw7NuIGBpbnN0YWxsX2tlcmFzKClgIHF1ZSByZWFsaXphIHVuYSBpbnN0YWxhY2nDs24gcG9yIGRlZmF1bHQgZGUgYmFzYWRhIGVuIGVsIENQVS4KClNpIHRpZW5lbiB1bmEgR1BVIChwcm9jZXNhZG9yIGdyw6FmaWNvKSwgbGFzIHJlZGVzIGZ1bmNpb25hbiBtdWNobyBtw6FzIHLDoXBpZG8sIHBvcnF1ZSBsYSBHUFUgcGVybWl0ZSBvcHRpbWl6YXIgbGFzIG9wZXJhY2lvbmVzIG1hdHJpY2lhbGVzLiBQZXJvIG5lY2VzaXRhbiBpbnN0YWxhciBrZXJhcyBwYXJhIHF1ZSBjb3JyYSBlbiBlbCBsZW5ndWFqZSBkZSBsYSBHUFUgKCBfQ1VEQV8gKQoKCmBgYHtyfQojIGRldnRvb2xzOjppbnN0YWxsX2dpdGh1YigicnN0dWRpby9rZXJhcyIpCmxpYnJhcnkoa2VyYXMpCiMgaW5zdGFsbF9rZXJhcygpCmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGtuaXRyKQpgYGAKCgojIyBNbmlzdAoKRXN0ZSBlcyB1biBwcm9ibGVtYSBjbMOhc2ljbywgZGUganVndWV0ZSwgcXVlIHNpcnZlIGRlc2RlIGhhY2UgbXVjaG9zIGHDsW9zIGNvbW8gYmVuY2htYXJrIHBhcmEgY2xhc2lmaWNhY2nDs24gZGUgaW3DoWdlbmVzLiAKVGllbmUgNjAuMDAwIGltw6FnZW5lcywgZGUgMjh4MjgsIGRlIG7Dum1lcm9zIGVzY3JpdG9zIGEgbWFuby4KCmBgYHtyfQptbmlzdCA8LSBkYXRhc2V0X21uaXN0KCkKeF90cmFpbiA8LSBtbmlzdCR0cmFpbiR4CnlfdHJhaW4gPC0gbW5pc3QkdHJhaW4keQp4X3Rlc3QgPC0gbW5pc3QkdGVzdCR4CnlfdGVzdCA8LSBtbmlzdCR0ZXN0JHkKYGBgCgoKVmVhbW9zIGxhIHBpbnRhIGRlIGxvcyBkYXRvcwoKX19kYXRvcyBkZSBlbnRyYWRhX18KCmBgYHtyfQptYXRyaXgucm90YXRlIDwtIGZ1bmN0aW9uKGltZykgeyAKICAgIHQoYXBwbHkoaW1nLCAyLCByZXYpKQp9CgpwYXIobWZyb3c9YygzLCAzKSkKZm9yIChpZHggaW4gMTo5KSB7CiAgICBsYWJlbCA8LSB5X3RyYWluW2lkeF0KICAgIGltYWdlKG1hdHJpeC5yb3RhdGUoeF90cmFpbltpZHgsLF0pLCBjb2wgPSBncmV5KGxldmVsID0gc2VxKDEsIDAsIGJ5PS0xLzI1NSkpLCBheGVzPUYsIG1haW49bGFiZWwpCiAgCn0KYGBgCgoKPiBQYXLDoSBwYXLDoSBwYXLDoS4uLiB2b3MgbWUgcXVlcsOpcyBkZWNpciBxdWUgYWhvcmEgY2FkYSBvYnNlcnZhY2nDs24gZXMgdW5hIG1hdHJpeiBkZSAyOHgyOD8KCj4gU0kKCgotIEVuIGVzdGFkw61zdGljYSB5IE1hY2hpbmUgTGVhcm5pbmcgZXN0YW1vcyBhY29zdHVtYnJhZG9zIGEgcGVuc2FyIGNhZGEgb2JzZXJ2YWNpw7NuIGNvbW8gdW5hIGZpbGEsIGNvbiBmZWF0dXJlcyBlbiBsYXMgY29sdW1uYXMuIFNpIHF1ZXJlbW9zIHJlcHJlc2VudGFyIHVuYSBpbWFnZW4gdGVuZW1vcyBxdWUgX2FwbGFuYXJsYV8uIEVzdG8gc2lnbmlmaWNhIHF1ZSB0ZW5kcsOtYW1vcyAyOHgyOD03ODQgZmVhdHVyZXMuIFByb2JhYmxlbWVudGUgYSBjb250aW51YWNpw7NuIG5lY2VzaXRhcsOtYW1vcyB1bmEgdMOpY25pY2EgZGUgcmVkdWNjacOzbiBkZSBsYSBkaW1lbnNpb25hbGlkYWQsIHkgcG9yIGFow60gc2VndWltb3MuLi4KCi0gRW4gbGFzIHJlZGVzIG5ldXJvbmFsZXMgZXN0byBjYW1iaWEuIFNpIGJpZW4gZW4gZWwgbW9kZWxvIGRlIGhveSAoRnVsbHkgY29uZWN0ZWQgbGF5ZXJzKSBsYSBjYXBhIGRlIGVudHJhZGEgc2lndWUgdGVuaWVuZG8gNzg0IG5vZG9zLCBsYSBlc3RydWN0dXJhIGRlIGxhIHJlZCBwZXJtaXRlIGNhcHRhciBtdXkgYmllbiBsYXMgcmVsYWNpb25lcyBubyBsaW5lYWxlcyBwcm9waWFzIGRlIHVuYSBpbWFnZW4uIAoKLSBJbmNsdXNvIG3DoXMsIGN1YW5kbyB2ZWFtb3MgbGFzIENvbnZvbHV0aW9uYWwgTmV1cmFsIE5ldHdvcmtzLCB2YW1vcyBhIHV0aWxpemFyIGltYWdlbmVzIGEgY29sb3IsIGxvIHF1ZSBpbXBsaWNhIDMgbWF0cmljZXMgcGFyYSBsb3MgMyBjYW5hbGVzIGRlIGNvbG9yIChSR0IpCgoKX1/Cv3F1w6kgcGludGEgdGllbmUgdW4gZ3LDoWZpY28gZGVzZGUgZWwgcHVudG8gZGUgdmlzdGEgbWF0cmljaWFsP19fCgoKYGBge3J9CmthYmxlKGRhdGEuZnJhbWUoeF90cmFpblsxLCxdKSkKYGBgCgoKCkNhZGEgdmFsb3IgcmVwcmVzZW50YSBhIHVuIHBpeGVsLCB5IHN1IHZhbG9yIGVzIHN1IHJlcHJlc2VudGFjacOzbiBlbiBsYSBlc2NhbGEgZGUgZ3Jpc2VzIChkZSAwIGEgMjU1KS4gRXMgZGVjaXIsIGN1YW5kbyBtYXlvciBlcyBlbCB2YWxvciwgZXNvIGVzIHVuIHBpeGVsIG3DoXMgb3NjdXJvLiAKCkVsIGRhdG8gZXN0YSBlbiB1biBhcnJheSBkZSAzIGRpbWVuc2lvbmVzIChpbWFnZW4sYW5jaG8sbGFyZ28pLiBDb21vIHRlbmVtb3MgNjBLIGltYWdlbmVzLCBlc3RvIHRpZW5lIGxhIGZvcm1hIGRlIDoKCmBgYHtyfQpkaW0oeF90cmFpbikKYGBgCgoKRXMgZGVjaXIsIG51ZXN0cm9zIGRhdG9zIGFob3JhIGNvbmZvcm1hbiB1biBUZW5zb3I6CgohW1RlbnNvcl5baHR0cHM6Ly9oYWNrZXJub29uLmNvbS9sZWFybmluZy1haS1pZi15b3Utc3Vjay1hdC1tYXRoLXA0LXRlbnNvcnMtaWxsdXN0cmF0ZWQtd2l0aC1jYXRzLTI3ZjAwMDJjOWIzMl1dKGltZy9UZW5zb3IuanBlZykKCgpDb21vIG1lbmNpb25hYmFtb3MgYXJyaWJhIGxvIHByaW1lcm8gZXMgdW4gX3Jlc2hhcGVfIGRlIGxvcyBkYXRvczoKCi0gUGFzYXIgZGUgZXN0b3MgYXJyYXlzIGRlIDMgZGltZW5zaW9uZXMgYSAyIGRpbWVuc2lvbmVzIChjb21vIGVzdGFtb3MgYWNvc3R1bWJyYWRvcywgdW4gdmVjdG9yIHBvciBvYnNlcnZhY2nDs24pCi0gQWRlbcOhcywgbmVjZXNpdGFtb3MgY29udmVydGlyIGxhIGVzY2FsYSBkZSBsb3MgZGF0b3MgZGUgw61udGVnZXJzIGVudHJlIDAgeSAyNTUgYSBudW1lcm9zIGZsb2F0aW5nIHBvaW50IGVudHJlIDAgeSAxCgpgYGB7cn0KIyByZXNoYXBlCnhfdHJhaW4gPC0gYXJyYXlfcmVzaGFwZSh4X3RyYWluLCBjKG5yb3coeF90cmFpbiksIDI4KjI4KSkgI2xhIHByaW1lcmEgZGltZW5zacOzbiBlcyB0YW4gbGFyZ2EgY29tbyBsYSBjYW50aWRhZCBkZSBvYnNlcnZhY2lvbmVzLCBsYSBzZWd1bmRhIGRpbWVuc2nDs24gZXMgbGEgbWF0cml6IGFwbGFuYWRhICgyOCoyOCkKeF90ZXN0IDwtIGFycmF5X3Jlc2hhcGUoeF90ZXN0LCBjKG5yb3coeF90ZXN0KSwgMjgqMjgpKQojIHJlc2NhbGUKeF90cmFpbiA8LSB4X3RyYWluIC8gMjU1CnhfdGVzdCA8LSB4X3Rlc3QgLyAyNTUKYGBgCgoKb2JzOiBlc3RvIGxvIHBvZHLDrWFtb3MgaGFjZXIgY29uIGBkaW0gPC1gLCBwZXJvIHNlcsOtYSBtZW5vcyBwZXJmb3JtYW50ZS4KCgpfX2RhdG9zIGRlIHNhbGlkYV9fCgpgYGB7cn0KeV90cmFpbiAlPiUgaGVhZCguKQpgYGAKCkVzIHVuIHZlY3RvciBkZSBpbnRlZ2VycyBlbnRyZSAwLTkuIAoKRGFkYSBsYSBpbXBsZW1lbnRhY2nDs24gZGUgbGFzIHJlZGVzLCBuZWNlc2l0YW1vcyBwYXNhcmxvIGEgX19vbmUtaG90IGVuY29kaW5nX18gZXN0byBzZSBoYWNlIGNvbiBsYSBmdW5jacOzbiBgdG9fY2F0ZWdvcmljYWwoKWAgZGUgS2VyYXMKCgpgYGB7cn0KeV90cmFpbiA8LSB0b19jYXRlZ29yaWNhbCh5X3RyYWluLCAxMCkKeV90ZXN0IDwtIHRvX2NhdGVnb3JpY2FsKHlfdGVzdCwgMTApCmBgYAoKCsK/cXXDqSBwaW50YSB0aWVuZSBlc3RvPwoKYGBge3J9CnlfdHJhaW4gJT4lIGhlYWQoLikKYGBgCgoKCgojIyBEZWZpbmNpw7NuIGRlbCBtb2RlbG8KCgpQYXJhIGFybWFyIGVsIG1vZGVsbyBwcmltZXJvIGRlZmluaW1vcyBlbCB0aXBvIGRlIG1vZGVsby4gUGFyYSBlc28gdXNhbW9zIGBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKClgIHF1ZSBub3MgcGVybWl0ZSBzaW1wbGVtZW50ZSBhcGlsYXIgY2FwYXMgZGUgbGEgcmVkLiAKCi0gRW4gbGEgcHJpbWVyYSBjYXBhIHRlbmVtb3MgcXVlIGFjbGFyYXIgZWwgaW5wdXRfc2hhcGUuIEVuIGVzdGUgY2FzbyBlbCB1bmlkaW1lbnNpb25hbCAobG8gYXBsYW5hbW9zIHJlY2nDqW4pLCBwZXJvIHBvZHLDrWEgc2VyIHVuIHRlbnNvciBkZSBjdWFscXVpZXIgZGltZW5zacOzbiAoISEpCi0gTGFzIGNhcGFzIHNlIGFncmVnYW4gY29uIHBpcGVzIGAlPiVgCi0gTGEgw7psdGltYSBjYXBhIHRpZW5lIGxhIG1pc21hIGNhbnRpZGFkIGRlIHVuaWRhZGVzIHF1ZSBjYXRlZ29yw61hcyBudWVzdHJvIG91dHB1dC4gTGEgc2FsaWRhIGRlbCBtb2RlbG8gZXMgdW4gdmVjdG9yIHF1ZSBhc2lnbmEgdW5hIHByb2JhYmlsaWRhZCBhIGNhZGEgdW5hIGRhIGxhcyBjYXRlZ29yw61hcwotIEVuIGNhZGEgY2FwYSB0ZW5lbW9zIHF1ZSBkZWZpbmlyIHVuYSBmdW5jacOzbiBkZSBhY3RpdmFjacOzbgotIEFkZW3DoXMgYWdyZWdhbW9zIHVuYSByZWd1bGFyaXphY2nDs24gYGxheWVyX2Ryb3B1dCh4KWAgcXVlIGxvIHF1ZSBoYWNlIGVzLCBlbiBjYWRhIGl0ZXJhY2nDs24gZGVsIGFqdXN0ZSwgaWdub3JhciBlbCB4JSBkZSBsYXMgY29uZXhpb25lcy4gRXN0byBldml0YSBlbCBzb2JyZWFqdXN0ZSBkZWwgbW9kZWxvCgpgYGB7cn0KCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAKbW9kZWwgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gMjU2LCBhY3RpdmF0aW9uID0gJ3JlbHUnLCBpbnB1dF9zaGFwZSA9IGMoNzg0KSkgJT4lIAogIGxheWVyX2Ryb3BvdXQocmF0ZSA9IDAuNCkgJT4lIAogIGxheWVyX2RlbnNlKHVuaXRzID0gMTI4LCBhY3RpdmF0aW9uID0gJ3JlbHUnKSAlPiUKICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjMpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAnc29mdG1heCcpCmBgYAoKCiMjIyMgRnVuY2lvbmVzIGRlIGFjdGl2YWNpw7NuCgpQYXJhIGVzdGUgbW9kZWxvIHV0aWxpemFtb3MgZG9zIGZ1bmNpb25lcyBkZSBhY3RpdmFjacOzbjogCgotIFJlY3RpZmllZCBMaW5lYXIgVW5pdDogJCRmKHgpPW1heCgwLHgpJCQKLSBTb2Z0bWF4IDogJCQgZih4KT1cZnJhY3tlXnt4X2l9fXtcc3VtX3tqPTF9Xm4gZV57eF9qfX0kJAoKRGVmaW5pZGFzIGVuIGPDs2RpZ28geSBncsOhZmljYW1lbnRlOgoKYGBge3J9CgpyZWx1IDwtIGZ1bmN0aW9uKHgpIGlmZWxzZSh4ID49IDAsIHgsIDApCnNvZnRtYXggPC0gZnVuY3Rpb24oeCkgZXhwKHgpIC8gc3VtKGV4cCh4KSkKCmRhdGEuZnJhbWUoeD0gc2VxKGZyb209LTEsIHRvPTEsIGJ5PTAuMSkpICU+JSAKICBtdXRhdGUoc29mdG1heCA9IHNvZnRtYXgoeCksCiAgICAgICAgIHJlbHUgPSByZWx1KHgpKSAlPiUgCiAgZ2F0aGVyKHZhcmlhYmxlLHZhbHVlLDI6MykgJT4lIAoKZ2dwbG90KC4sIGFlcyh4PXgsIHk9dmFsdWUsIGdyb3VwPXZhcmlhYmxlLCBjb2xvdXI9dmFyaWFibGUpKSsKICBnZW9tX2xpbmUoc2l6ZT0xKSArCiAgZ2d0aXRsZSgiUmVMVSAmIFNvZnRtYXgiKSsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgoKX19SZUx1X18gZXMgbGEgZnVuY2nDs24gZGUgYWN0aXZhY2nDs24gcXVlIG3DoXMgc2UgdXRpbGl6YSBlbiBsYSBhY3R1YWxpZGFkLiAKClNpIHF1ZXJlbW9zIHZlciB1biByZXN1bWVuIGRlbCBtb2RlbG86CgpgYGB7ciBlY2hvPVQsIHJlc3VsdHM9J2hpZGUnLGV2YWw9Rn0Kc3VtbWFyeShtb2RlbCkKYGBgCgohW10oaW1nL2ZjX21vZGVsX3N1bW1hcnkucG5nKQoKCkVsIG1vZGVsbyB0aWVuZSAyMzUsMTQ2IHBhcsOhbWV0cm9zIHBhcmEgb3B0aW1pemFyOgoKRW4gbGEgcHJpbWVyYSBjYXBhIG9jdWx0YSB0ZW5lbW9zIDI1NiBub2RvcyBxdWUgc2UgY29uZWN0YW4gY29uIGNhZGEgbm9kbyBkZSBsYSBjYXBhIGRlIGVudHJhZGEgKDc4NCksIGFkZW3DoXMgZGUgdW4gYmlhcyBwYXJhIGNhZGEgbm9kbzoKYGBge3J9Cjc4NCoyNTYrMjU2CmBgYAoKTGEgY2FwYSBkZSBkcm9wdXQgZXMgdW5hIHJlZ3VsYXJpemFjacOzbiB5IG5vIGFqdXN0YSBuaW5nw7puIHBhcsOhbWV0cm8KCmxhIGNhcGEgZGVuc2EgMiBzZSBjb25lY3RhIGNvbiBsb3MgMjU2IG5vZG9zIGRlIGxhIHByaW1lcmEgY2FwYToKCmBgYHtyfQoxMjgqMjU2KzEyOApgYGAKCkxhIHRlcmNlcmEgY2FwYQpgYGB7cn0KMTI4KjEwKzEwCmBgYAoKLS0tLS0tLS0tLS0KCkx1ZWdvIG5lY2VzaXRhbW9zIF9fY29tcGlsYXIgZWwgbW9kZWxvX18gaW5kaWNhbmRvIGxhIGZ1bmNpw7NuIGRlIF9sb3NzXywgcXXDqSB0aXBvIGRlIG9wdGltaXphZG9yIHV0aWxpemFyLCB5IHF1w6kgbcOpdHJpY2FzIG5vcyBpbXBvcnRhbgoKYGBge3J9Cm1vZGVsICU+JSBjb21waWxlKAogIGxvc3MgPSAnY2F0ZWdvcmljYWxfY3Jvc3NlbnRyb3B5JywKICBvcHRpbWl6ZXIgPSBvcHRpbWl6ZXJfcm1zcHJvcCgpLAogIG1ldHJpY3MgPSBjKCdhY2N1cmFjeScpCikKYGBgCgoKIyMgRW50cmVuYW1pZW50bwoKUGFyYSBhanVzdGFyIGVsIG1vZGVsbyB1c2Ftb3MgbGEgZnVuY2nDs24gYGZpdCgpYCwgYWPDoSBuZWNlc2l0YW1vcyBwYXNhciBsb3Mgc2lndWllbnRlcyBwYXLDoW1ldHJvczoKCi0gRWwgYXJyYXkgY29uIGxvcyBkYXRvcyBkZSBlbnRyZW5hbWllbnRvCi0gRWwgYXJyYXkgY29uIGxvcyBvdXRwdXRzCi0gYGVwb2Noc2A6IEN1YW50YXMgdmVjZXMgdmEgYSByZWNvcnJlciBlbCBkYXRhc2V0IGRlIGVudHJlbmFtaWVudG8KLSBgYmF0Y2hfc2l6ZWA6IGRlIGEgY3VhbnRhcyBpbWFnZW5lcyB2YSBhIG1pcmFyIGVuIGNhZGEgaXRlcmFjacOzbiBkZWwgYmFja3Byb3BhZ2F0aW9uCi0gYHZhbGlkYXRpb25fc3BsaXRgOiBIYWNlbW9zIHVuIHNwbGl0IGVuIHRyYWluIHkgdmFsaWRhdGlvbiBwYXJhIGV2YWx1YXIgbGFzIG3DqXRyaWNhcy4KCmBgYHtyIGVjaG89VCwgcmVzdWx0cz0naGlkZScsZXZhbD1GfQpmaXRfaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sIHlfdHJhaW4sIAogIGVwb2NocyA9IDMwLCBiYXRjaF9zaXplID0gMTI4LCAKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yCikKYGBgCgohW10oaW1nL2ZjX3RyYWluaW5nLnBuZykKCgoKYGBge3IgaW5jbHVkZT1GQUxTRSwgZXZhbD1GfQojZ3VhcmRvIGxhIGhpc3RvcmlhLiBObyBsbyBtdWVzdHJvLCBuaSBsbyBjb3JybyBwb3IgZGVmYXVsdApzYXZlUkRTKGZpdF9oaXN0b3J5LCIuLi9SZXN1bHRhZG9zL2ZjX2hpc3QuUkRTIikKYGBgCgpgYGB7ciBpbmNsdWRlPUZBTFNFLCBldmFsPVR9CiNsZXZhbnRvIGxhIGhpc3RvcmlhLiBObyBsbyBtdWVzdHJvLCBwZXJvIGxvIGNvcnJvIHBvciBkZWZhdWx0CmZpdF9oaXN0b3J5IDwtIHJlYWRfcmRzKCIuLi9SZXN1bHRhZG9zL2ZjX2hpc3QuUkRTIikKYGBgCgoKCk1pZW50cmFzIGVudHJlbmFtb3MgZWwgbW9kZWxvLCBwb2RlbW9zIHZlciBsYSBldm9sdWNpw7NuIGVuIGVsIGdyw6FmaWNvIGludGVyYWN0aXZvIHF1ZSBzZSBnZW5lcmEgZW4gZWwgdmlld2VyIGRlIFJzdHVkaW8uCgpgZml0KClgIG5vcyBkZXZ1ZWx2ZSB1biBvYmpldG8gcXVlIGluY2x1eWUgbGFzIG3DqXRyaWNhcyBkZSBsb3NzIHkgYWNjdXJhY3kKCmBgYHtyfQpmaXRfaGlzdG9yeQpgYGAKCkVzdGUgb2JqZXRvIGxvIHBvZGVtb3MgZ3JhZmljYXIgY29uIGBwbG90KClgIHkgbm9zIGRldnVlbHZlIHVuIG9iamV0byBkZSBfZ2dwbG90Xywgc29icmUgZWwgcXVlIHBvZGVtb3Mgc2VndWlyIHRyYWJhamFuZG8KCmBgYHtyfQpwbG90KGZpdF9oaXN0b3J5KSsKICB0aGVtZV9taW5pbWFsKCkrCiAgbGFicyh0aXRsZT0gIkV2b2x1Y2nDs24gZGUgTG9zcyB5IEFjY3VyYWN5IGVuIHRyYWluIHkgdmFsaWRhdGlvbiIpCmBgYAoKCgo+IE5vdGVuIHF1ZSBlbCBtb2RlbG8gZW50cmVuYWRvLCBjb24gZWwgcXVlIHBvZGVtb3MgcHJlZGVjaXIsIHNpZ3VlIHNpZW5kbyBgbW9kZWxgLgoKX19lcyBpbXBvcnRhbnRlIGd1YXJkYXIgZWwgbW9kZWxvIGx1ZWdvIGRlIGVudHJlbmFyLCBwYXJhIHBvZGVyIHJldXRpbGl6YXJsb19fCgoKYGBge3IgZXZhbD1GQUxTRSwgZWNobz1UfQptb2RlbCAlPiUgc2F2ZV9tb2RlbF9oZGY1KCIuLi9SZXN1bHRhZG9zL2ZjX21vZGVsLmg1IikKYGBgCgp5IHBhcmEgY2FyZ2FybG8KCmBgYHtyfQptb2RlbG9fcHJlZW50cmVuYWRvIDwtIGxvYWRfbW9kZWxfaGRmNSgiLi4vUmVzdWx0YWRvcy9mY19tb2RlbC5oNSIpCmBgYAoKYGBge3J9Cm1vZGVsb19wcmVlbnRyZW5hZG8KYGBgCgpTaSBxdWVyZW1vcyBldmFsdWFyIGVsIG1vZGVsbyBzb2JyZSBlbCBjb25qdW50byBkZSB0ZXN0IChkaXN0aW50byBkZWwgZGUgdmFsaWRhY2nDs24pIHBvZGVtb3MgdXNhciBsYSBmdW5jacOzbiBgZXZhbHVhdGUoKWAKCmBgYHtyfQptb2RlbG9fcHJlZW50cmVuYWRvICU+JSBldmFsdWF0ZSh4X3Rlc3QsIHlfdGVzdCkKYGBgCgpQYXJhIG9idGVuZXIgbGFzIHByZWRpY2Npb25lcyBzb2JyZSB1biBudWV2byBjb25qdW50byBkZSBkYXRvcyB1dGlsaXphbW9zIGBwcmVkaWN0X2NsYXNzZXMoKWAKCmBgYHtyfQptb2RlbG9fcHJlZW50cmVuYWRvICU+JSBwcmVkaWN0X2NsYXNzZXMoeF90ZXN0KSAlPiUgaGVhZCguKQpgYGAKCgojIyBPdHJvcyByZWN1cnNvcyBpbnRlcmVzYW50ZXM6CgpbVmlzdWFsaXphY2nDs24gZGUgdW5hIFJlZCBGdWxseSBjb25lY3RlZCBwYXJhIGNsYXNpZmljYWNpw7NuIGRlIGTDrWdpdG9zXShodHRwOi8vc2NzLnJ5ZXJzb24uY2EvfmFoYXJsZXkvdmlzL2ZjLykKCgpbVGVuc29yIEZsb3cgUGxheWdyb3VuZF0oaHR0cDovL3BsYXlncm91bmQudGVuc29yZmxvdy5vcmcvI2FjdGl2YXRpb249dGFuaCZiYXRjaFNpemU9MTAmZGF0YXNldD1jaXJjbGUmcmVnRGF0YXNldD1yZWctcGxhbmUmbGVhcm5pbmdSYXRlPTAuMDMmcmVndWxhcml6YXRpb25SYXRlPTAmbm9pc2U9MCZuZXR3b3JrU2hhcGU9NCwyJnNlZWQ9MC41OTc5NCZzaG93VGVzdERhdGE9ZmFsc2UmZGlzY3JldGl6ZT1mYWxzZSZwZXJjVHJhaW5EYXRhPTUwJng9dHJ1ZSZ5PXRydWUmeFRpbWVzWT1mYWxzZSZ4U3F1YXJlZD1mYWxzZSZ5U3F1YXJlZD1mYWxzZSZjb3NYPWZhbHNlJnNpblg9ZmFsc2UmY29zWT1mYWxzZSZzaW5ZPWZhbHNlJmNvbGxlY3RTdGF0cz1mYWxzZSZwcm9ibGVtPWNsYXNzaWZpY2F0aW9uJmluaXRaZXJvPWZhbHNlJmhpZGVUZXh0PWZhbHNlCikKCgpbRGlhZ3JhbWFzIGRlIHJlZGVzXShodHRwOi8vYWxleGxlbmFpbC5tZS9OTi1TVkcvaW5kZXguaHRtbCkKCgoK