suppressMessages(library(EBImage))
suppressMessages(library(rgl))
suppressMessages(library(dplyr))
suppressMessages(library(tidyr))
suppressMessages(library(ggplot2))
suppressMessages(library(h2o))

Resumen

MNIST (“Instituto Nacional Modificado de Estándares y Tecnología”) es el conjunto de datos de facto de “visión mundial” de la visión de computadora. Desde su lanzamiento en 1999, este clásico conjunto de datos de imágenes manuscritas ha servido como base para los algoritmos de clasificación de referencia. A medida que surgen nuevas técnicas de aprendizaje automático, MNIST sigue siendo un recurso confiable para investigadores y estudiantes por igual.

El conjunto de datos mixto de Instituto Nacional de estándares y tecnología (MNIST) es una colección de 70.000 pequeñas imágenes de dígitos escritos a mano. Los datos fue creados para actuar como un referente para los algoritmos de reconocimiento de imagen. Aunque MNIST las imágenes son pequeñas (28 x 28 pixeles) y sólo hay 10 dígitos posibles (cero a nueve) a reconocer y hay 42.0000 imágenes de formación para la creación de un modelo de reconocimiento de imagen (con 28.000 imágenes tendidas a probar la exactitud de un modelo), la experiencia ha demostrado que reconocer las imágenes MNIST es un problema difícil.

Para lidiar con este problema vamos a extraer características de cada imagen, con esto lograremos disminuir la cantidad de datos de entrenamiento. Luego implementaremos una red neuronal con el paquete H2O.

Extraccion de características

Al revisar los datos tenemos un archivo con 42000 imágenes con 785 variables (28x28 píxeles) cada una, esto resulta en 33 millones de datos aproximadamente. Donde la primera columna es el número en la imagen y las 784 restantes corresponden a la imagen del número, como se puede ver en la imagen siguiente (primeros 250 dígitos).

# lee archivo con datos de entrenamiento (imagenes de 28x28)
train <- read.csv("train.csv")
train$label <- factor(train$label)
train[,c(2:785)] <- round(train[,c(2:785)], digits = 0)

dim(train)
[1] 42000   785
str(train[,1:10])
'data.frame':   42000 obs. of  10 variables:
 $ label : Factor w/ 10 levels "0","1","2","3",..: 2 1 2 5 1 1 8 4 6 4 ...
 $ pixel0: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel1: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel2: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel3: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel4: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel5: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel6: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel7: num  0 0 0 0 0 0 0 0 0 0 ...
 $ pixel8: num  0 0 0 0 0 0 0 0 0 0 ...
l <- 1
for (i in 1:10) {
      for (k in 1:25) {
            if(k==1){
                  b <- matrix(unlist(train[l,-1])/255, ncol = 28, nrow = 28)
            }
            if(k>1){
                  a <- matrix(unlist(train[l,-1])/255, ncol = 28, nrow = 28)
                  b <- rbind(b,a) 
            }
            l <- l+1
      }
      if(i==1){
            c <- b
            remove(b)
      }
      if(i>1){
            c <- cbind(c,b)
            remove(b)
      }
}

display(colormap(c, palette = topo.colors(256)), method = "raster")
title("Primeros 250 dígitos escritos a mano")

remove(train)
# Carga características Px, Py, Sx, Sy
train <- readRDS("caract_1_data.rds")
# Carga distancia respecto a referencia
t <- readRDS("caract_2_data.rds")
# Une ambas carateristicas en un objeto
train <- cbind(train, t[,-1])
remove(t)
train$label <- as.factor(train$label)

Dependiendo de las capacidades del equipo que efectúe el modelo puede ser necesario disminuir la cantidad de variables, en este caso se realizará en un computador personal, por lo que se hace necesario disminuir la cantidad de datos.

Para disminuir la gran cantidad de datos es necesario realizar algunas transformaciones y extraer información resumida de la imagen. En este caso consideraremos los siguientes pasos, que han dado buenos resultados con otros modelos:

El superindice \(^c\) representa una imagen o medida asociada a la clase \(c\).

Con esto podemos reducir el número de dimensiones de 785 a 123, que representa alrededor de un 16% de los datos originales.

Nota: Las características anteriormente mensionadas fueron cargadas a partir de otros modelos. Para revisar el código en detalle ver Distancia y Suma Vertical/Horizaontal y Diferencia Vertical/Horizantal

A continuación podemos observar la estructura de cada característica para cada clase, observando distribuciones ligeramente diferentes en cada caso, esto nos servirá a la hora de clasificar cada clase.

t2 <- gather(train, key = caracteristica, 
             value = valor, Py1:eucl9, factor_key = TRUE)
a1 <- dim(train)[1]*28
a2 <- dim(train)[1]*28*2
a3 <- dim(train)[1]*28*3
a4 <- dim(train)[1]*28*4
a5 <- dim(train)[1]*10

ggplot(data=t2[(1+a1):a2,], aes(caracteristica, valor,color=label)) +
      geom_boxplot() + facet_grid(label~.) + 
      ggtitle("Característica: Suma píxeles eje x") +
      theme(axis.text.x = element_text(angle = 90))

ggplot(data=t2[1:a1,], aes(caracteristica, valor,color=label)) +
      geom_boxplot() + facet_grid(label~.) + 
      ggtitle("Característica: Suma píxeles eje y") +
      theme(axis.text.x = element_text(angle = 90))

ggplot(data=t2[(1+a2):a3,], aes(caracteristica, valor,color=label)) +
      geom_boxplot() + facet_grid(label~.) + 
      ggtitle("Característica: Suma de diferencia de píxeles eje x") +
      theme(axis.text.x = element_text(angle = 90))

ggplot(data=t2[(1+a3):a4,], aes(caracteristica, valor,color=label)) +
      geom_boxplot() + facet_grid(label~.) + 
      ggtitle("Característica: Suma de diferencia de píxeles eje y") +
      theme(axis.text.x = element_text(angle = 90))

ggplot(data=t2[(1+a4):(a4+a5),], aes(caracteristica, valor,color=label)) +
      geom_boxplot() + facet_grid(label~.) + 
      ggtitle("Característica: Distancia por cada dígito") +
      theme(axis.text.x = element_text(angle = 90))

remove(t2, a1, a2, a3, a4, a5)

Red Neuronal

Para desarrollar la red neuronal que clasifique dígitos vamos a utilizar el paquete H2O.

H2O es un producto creado por la compañía H2O.ai con el objetivo de combinar los principales algoritmos de machine learning y aprendizaje estadístico con el Big Data. Gracias a su forma de comprimir y almacenar los datos, H2O es capaz de trabajar con millones de registros en un único ordenador (emplea todos sus cores) o en un cluster de muchos ordenadores. Internamente, H2O está escrito en Java y sigue el paradigma Key/Value para almacenar los datos y Map/Reduce para sus algoritmos.

Para más detalles sobre este paquete revisar (Machine Learning con H2O y R).

Comenzamos inicializando el cluster (local). Para esto utilizamos la función h2o.init. Tras iniciar el cluster (local), se muestran sus características, entre las que están: el número de cores activados (4), la memoria total del cluster (3.56GB), el número de nodos (1 porque se está empleando un único ordenador).

Luego procedemos a dividir los datos en conjunto de entrenamiento y pruebas (70% y 30% respectivamente). Estos valores se consideraron debido a que generalmente entregan buenos resultados y para poder compara resultados con otros métodos (ver Referencia).

Además, para la optimización de los hiperparámetrors, se considerará validación cruzada con 10 folds.

# Creación de un cluster local con todos los cores disponibles.
h2o.init(nthreads = -1, # Utiliza todoslos cores disponibles
         max_mem_size = "4g") # Máxima memoria disponible para el cluster.

H2O is not running yet, starting it now...

Note:  In case of errors look at the following log files:
    C:\Users\HP\AppData\Local\Temp\Rtmp2zc8bT/h2o_HP_started_from_r.out
    C:\Users\HP\AppData\Local\Temp\Rtmp2zc8bT/h2o_HP_started_from_r.err


Starting H2O JVM and connecting: .. Connection successful!

R is connected to the H2O cluster: 
    H2O cluster uptime:         4 seconds 988 milliseconds 
    H2O cluster timezone:       America/Santiago 
    H2O data parsing timezone:  UTC 
    H2O cluster version:        3.22.1.1 
    H2O cluster version age:    2 months and 8 days  
    H2O cluster name:           H2O_started_from_R_HP_sdd455 
    H2O cluster total nodes:    1 
    H2O cluster total memory:   3.56 GB 
    H2O cluster total cores:    4 
    H2O cluster allowed cores:  4 
    H2O cluster healthy:        TRUE 
    H2O Connection ip:          localhost 
    H2O Connection port:        54321 
    H2O Connection proxy:       NA 
    H2O Internal Security:      FALSE 
    H2O API Extensions:         Algos, AutoML, Core V3, Core V4 
    R Version:                  R version 3.5.2 (2018-12-20) 
train <- as.h2o(train)

  |                                                                       
  |                                                                 |   0%
  |                                                                       
  |=================================================================| 100%
# Crea conjunto de entrenamiento y pruebas (70% y 30%)
Index1 <- h2o.splitFrame(data = train, ratios = c(0.7), seed = 28916022)
TRAIN   <- h2o.assign(data = Index1[[1]], key = "TRAIN")
TEST    <- h2o.assign(data = Index1[[2]], key = "TEST")
remove(train, Index1)

Para el desarrollo de la red neuronal se considera una arquitectura de 4 capas ocultas con 90, 70, 100 y 25 neuronas respectivamente, una capa inicial con 122 neuronas y una capa de salida con 10 neuronas.

Además se consideran:

mod_1 = h2o.deeplearning(y = 'label', # Variable a clasificar
                         training_frame = TRAIN, # conjunto de entrenamiento
                         activation = 'Rectifier', # funcion activacion
                         hidden = c(90, 70, 100, 25), # arquitectura red neuronal
                         epochs = 3000, # epocas
                         train_samples_per_iteration = -1,# Datos a iterar por epoca
                         nfolds = 10, # nfold de validación cruzada
                         seed = 300, # semilla
                         rho = 0.9999, # Adaptive learning rate
                         standardize = TRUE, # normalizacion variables entrada
                         distribution = "multinomial", # funcion de distribucion
                         model_id = "mod_1") #id

  |                                                                       
  |                                                                 |   0%
  |                                                                       
  |===========                                                      |  17%
  |                                                                       
  |======================                                           |  33%
  |                                                                       
  |======================                                           |  34%
  |                                                                       
  |===========================                                      |  42%
  |                                                                       
  |================================                                 |  50%
  |                                                                       
  |=================================                                |  50%
  |                                                                       
  |===========================================                      |  67%
  |                                                                       
  |======================================================           |  83%
  |                                                                       
  |=================================================================| 100%
saveRDS(mod_1, "NN.rds")

Una vez ajustada la red, podemos ver los detalles del modelo y la evolución del error por época.

mod_1
Model Details:
==============

H2OMultinomialModel: deeplearning
Model ID:  mod_1 
Status of Neuron Layers: predicting label, 10-class classification, multinomial distribution, CrossEntropy loss, 27.325 weights/biases, 340,1 KB, 2.441.030 training samples, mini-batch size 1
  layer units      type dropout       l1       l2 mean_rate rate_rms
1     1   122     Input  0.00 %       NA       NA        NA       NA
2     2    90 Rectifier  0.00 % 0.000000 0.000000  0.403624 0.294657
3     3    70 Rectifier  0.00 % 0.000000 0.000000  0.016196 0.022172
4     4   100 Rectifier  0.00 % 0.000000 0.000000  0.017914 0.047780
5     5    25 Rectifier  0.00 % 0.000000 0.000000  0.007924 0.021313
6     6    10   Softmax      NA 0.000000 0.000000  0.083208 0.173294
  momentum mean_weight weight_rms mean_bias bias_rms
1       NA          NA         NA        NA       NA
2 0.000000    0.024170   0.539372 -0.765229 0.843749
3 0.000000   -0.136376   0.454879  0.905506 0.151904
4 0.000000   -0.098040   0.211586  0.835565 0.136051
5 0.000000   -0.053261   0.151750  1.030496 0.433357
6 0.000000   -0.170204   0.869860 -0.011475 2.908827


H2OMultinomialMetrics: deeplearning
** Reported on training data. **
** Metrics reported on temporary training frame with 10046 samples **

Training Set Metrics: 
=====================

MSE: (Extract with `h2o.mse`) 0.0002054122
RMSE: (Extract with `h2o.rmse`) 0.01433221
Logloss: (Extract with `h2o.logloss`) 0.001266773
Mean Per-Class Error: 0.0002076199
Confusion Matrix: Extract with `h2o.confusionMatrix(<model>,train = TRUE)`)
=========================================================================
Confusion Matrix: Row labels: Actual class; Column labels: Predicted class
         0    1   2    3   4   5   6    7   8   9  Error         Rate
0      996    0   0    0   0   0   0    0   0   0 0.0000 =    0 / 996
1        0 1115   0    0   0   0   0    0   0   0 0.0000 =  0 / 1.115
2        0    0 985    0   0   0   0    0   0   0 0.0000 =    0 / 985
3        0    0   0 1033   0   0   0    0   0   0 0.0000 =  0 / 1.033
4        0    0   0    0 994   0   0    0   0   0 0.0000 =    0 / 994
5        0    0   0    0   0 937   1    0   0   0 0.0011 =    1 / 938
6        0    0   1    0   0   0 989    0   0   0 0.0010 =    1 / 990
7        0    0   0    0   0   0   0 1054   0   0 0.0000 =  0 / 1.054
8        0    0   0    0   0   0   0    0 969   0 0.0000 =    0 / 969
9        0    0   0    0   0   0   0    0   0 972 0.0000 =    0 / 972
Totals 996 1115 986 1033 994 937 990 1054 969 972 0.0002 = 2 / 10.046

Hit Ratio Table: Extract with `h2o.hit_ratio_table(<model>,train = TRUE)`
=======================================================================
Top-10 Hit Ratios: 
    k hit_ratio
1   1  0.999801
2   2  0.999900
3   3  1.000000
4   4  1.000000
5   5  1.000000
6   6  1.000000
7   7  1.000000
8   8  1.000000
9   9  1.000000
10 10  1.000000



H2OMultinomialMetrics: deeplearning
** Reported on cross-validation data. **
** 10-fold cross-validation on training data (Metrics computed for combined holdout predictions) **

Cross-Validation Set Metrics: 
=====================

Extract cross-validation frame with `h2o.getFrame("TRAIN")`
MSE: (Extract with `h2o.mse`) 0.4236324
RMSE: (Extract with `h2o.rmse`) 0.6508705
Logloss: (Extract with `h2o.logloss`) 1.237227
Mean Per-Class Error: 0.4722773
Hit Ratio Table: Extract with `h2o.hit_ratio_table(<model>,xval = TRUE)`
=======================================================================
Top-10 Hit Ratios: 
    k hit_ratio
1   1  0.529174
2   2  0.588745
3   3  0.643795
4   4  0.698674
5   5  0.750221
6   6  0.800000
7   7  0.850459
8   8  0.901122
9   9  0.951989
10 10  1.000000


Cross-Validation Metrics Summary: 
                              mean          sd  cv_1_valid  cv_2_valid
accuracy                0.53360736    0.307169 0.110497236   0.9686067
err                      0.4663926    0.307169  0.88950276 0.031393297
err_count                   1384.7     913.633      2576.0        89.0
logloss                  1.2262098  0.76921827   2.3022053  0.14905031
max_per_class_error     0.53155977  0.33126286         1.0  0.06959707
mean_per_class_accuracy  0.5339588   0.3068614         0.1   0.9687072
mean_per_class_error    0.46604118   0.3068614         0.9 0.031292778
mse                      0.4196843  0.27605593   0.8095325 0.029865397
r2                       0.9496855 0.033083882  0.90363157   0.9963811
rmse                     0.5354491  0.25785518   0.8997403  0.17281608
                        cv_3_valid cv_4_valid  cv_5_valid cv_6_valid
accuracy                0.09701242 0.09306204  0.96561116 0.10418761
err                      0.9029876 0.90693796 0.034388833  0.8958124
err_count                   2690.0     2719.0       101.0     2674.0
logloss                  2.3380961   2.316198  0.15284793  2.3041925
max_per_class_error            1.0        1.0 0.059210528        1.0
mean_per_class_accuracy        0.1        0.1   0.9654329        0.1
mean_per_class_error           0.9        0.9 0.034567103        0.9
mse                      0.8104737  0.8108624 0.032175865  0.8098513
r2                      0.90406084  0.9041004   0.9960191 0.90141946
rmse                    0.90026313   0.900479  0.17937632 0.89991736
                         cv_7_valid cv_8_valid  cv_9_valid cv_10_valid
accuracy                  0.9701695 0.09145527   0.9620297   0.9734423
err                     0.029830508  0.9085447 0.037970316 0.026557712
err_count                      88.0     2722.0       110.0        78.0
logloss                  0.13691843  2.3091776  0.13632053 0.117091365
max_per_class_error     0.051779937        1.0  0.07508533 0.059925094
mean_per_class_accuracy  0.97051406        0.1  0.96190417  0.97302973
mean_per_class_error    0.029485956        0.9  0.03809583 0.026970241
mse                     0.025795955 0.80967426 0.033565015 0.025046445
r2                         0.996882  0.9013245  0.99601245   0.9970238
rmse                      0.1606112   0.899819  0.18320757  0.15826069
plot(mod_1)

Pruebas

A continuación podemos ver la matriz de confusión del connjunto de entrenamiento, donde se observa una precisión del 99.95% (error del 0.05%).

h2o.confusionMatrix(mod_1, TRAIN)
Confusion Matrix: Row labels: Actual class; Column labels: Predicted class
          0    1    2    3    4    5    6    7    8    9  Error
0      2908    0    0    0    0    1    0    0    0    0 0.0003
1         0 3222    0    0    0    0    0    0    0    0 0.0000
2         0    0 2953    0    0    0    0    0    0    0 0.0000
3         0    0    0 3064    0    0    0    0    0    0 0.0000
4         0    0    0    0 2850    1    2    0    0    0 0.0011
5         0    0    0    0    0 2667    2    0    0    0 0.0007
6         0    0    1    0    0    0 2885    0    0    0 0.0003
7         0    0    0    0    0    0    0 3081    0    0 0.0000
8         0    0    0    0    0    0    0    0 2885    0 0.0000
9         0    0    0    0    0    0    0    0    0 2888 0.0000
Totals 2908 3222 2954 3064 2850 2669 2889 3081 2885 2888 0.0002
               Rate
0      =  1 / 2.909
1      =  0 / 3.222
2      =  0 / 2.953
3      =  0 / 3.064
4      =  3 / 2.853
5      =  2 / 2.669
6      =  1 / 2.886
7      =  0 / 3.081
8      =  0 / 2.885
9      =  0 / 2.888
Totals = 7 / 29.410

Al realizar la matriz de confusión al conjunto de pruebas tenemos una precisión del 97.30% (error del 2.69%).

h2o.confusionMatrix(mod_1, TEST)
Confusion Matrix: Row labels: Actual class; Column labels: Predicted class
          0    1    2    3    4    5    6    7    8    9  Error
0      1209    1    1    0    1    1    6    0    3    1 0.0114
1         0 1446    3    4    0    1    0    4    0    4 0.0109
2         1    4 1183    4    2    5   11    8    5    1 0.0335
3         1    1   12 1226    0   24    1    7    8    7 0.0474
4         3    4    2    0 1188    0    2    6    5    9 0.0254
5         3    1    4    7    0 1090   10    2    7    2 0.0320
6         3    3    5    0    5   10 1222    0    3    0 0.0232
7         1    2    8    5    1    2    0 1295    0    6 0.0189
8         1    0    7   11    2    4    4    1 1144    4 0.0289
9         2    1    2    7   11    0    0   13    7 1257 0.0331
Totals 1224 1463 1227 1264 1210 1137 1256 1336 1182 1291 0.0262
                 Rate
0      =   14 / 1.223
1      =   16 / 1.462
2      =   41 / 1.224
3      =   61 / 1.287
4      =   31 / 1.219
5      =   36 / 1.126
6      =   29 / 1.251
7      =   25 / 1.320
8      =   34 / 1.178
9      =   43 / 1.300
Totals = 330 / 12.590

Además, a continuación se muestran las 50 variables más importantes, donde se encuentran con mayor importancia a las asociadas a la distancia con la imagen de referencia.

a <- as.data.frame(h2o.varimp(mod_1))
ggplot(data=a[1:50,], aes(reorder(variable, -scaled_importance), scaled_importance)) +
      geom_point(color = "red") + ggtitle("Importancia relativa de las primeras 50 variables") +
      theme(axis.text.x = element_text(angle = 90)) + 
      ylab("Importancia") + xlab("Variable") + ylim(0,1)


Conclusiones

La reducción de dimensiones mediante la determincación de las carácterísticas anteriormente descritas no limitan el desempeño de la red y hacen posible una implementación liviana para un computador personal.

La red neuronal presenta un buen desempeño, tanto para el conjunto de entrenamiento como para el conjunto de pruebas, por lo que es posible de generalizar para otros digitos que no estén en el conjunto de entrenamiento.

Se puede considerar un modelo con aún menos variables, debido a que las distancia euclidea tienen mayor importancia relativa que el resto de variables.


Referencia


Información de sesión

sessionInfo()
R version 3.5.2 (2018-12-20)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 17134)

Matrix products: default

locale:
[1] LC_COLLATE=Spanish_Chile.1252  LC_CTYPE=Spanish_Chile.1252   
[3] LC_MONETARY=Spanish_Chile.1252 LC_NUMERIC=C                  
[5] LC_TIME=Spanish_Chile.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] h2o_3.22.1.1   ggplot2_3.1.0  tidyr_0.8.3    dplyr_0.8.0.1 
[5] rgl_0.99.16    EBImage_4.24.0

loaded via a namespace (and not attached):
 [1] locfit_1.5-9.1          tidyselect_0.2.5       
 [3] xfun_0.5                purrr_0.3.1            
 [5] lattice_0.20-38         colorspace_1.4-0       
 [7] miniUI_0.1.1.1          htmltools_0.3.6        
 [9] yaml_2.2.0              rlang_0.3.1            
[11] manipulateWidget_0.10.0 later_0.8.0            
[13] pillar_1.3.1            glue_1.3.0             
[15] withr_2.1.2             BiocGenerics_0.28.0    
[17] jpeg_0.1-8              plyr_1.8.4             
[19] stringr_1.4.0           munsell_0.5.0          
[21] gtable_0.2.0            htmlwidgets_1.3        
[23] evaluate_0.13           labeling_0.3           
[25] knitr_1.21              httpuv_1.4.5.1         
[27] crosstalk_1.0.0         parallel_3.5.2         
[29] Rcpp_1.0.0              xtable_1.8-3           
[31] promises_1.0.1          scales_1.0.0           
[33] webshot_0.5.1           jsonlite_1.6           
[35] abind_1.4-5             mime_0.6               
[37] png_0.1-7               digest_0.6.18          
[39] stringi_1.3.1           tiff_0.1-5             
[41] shiny_1.2.0             grid_3.5.2             
[43] tools_3.5.2             bitops_1.0-6           
[45] magrittr_1.5            RCurl_1.95-4.12        
[47] lazyeval_0.2.1          tibble_2.0.1           
[49] crayon_1.3.4            pkgconfig_2.0.2        
[51] assertthat_0.2.0        rmarkdown_1.11         
[53] R6_2.4.0                fftwtools_0.9-8        
[55] compiler_3.5.2