11 Redes neuronales

  • A este tipo de algorimos se les suele llamar de “caja negra”. La caja negra se debe a que los modelos subyacentes se basan en sistemas matemáticos complejos y los resultados son difíciles de interpretar.

  • Aunque puede no ser factible interpretar los modelos de caja negra, es peligroso aplicar los métodos a ciegas.

11.1 Entendiendo una red neuronal

  • Una Red Neural Artificial (ANN) modela la relación entre un conjunto de señales de entrada y una señal de salida.

  • ANN usa una red de neuronas o nodos artificiales para resolver problemas de aprendizaje.

En términos generales, las RNA (ANN) son aprendices versátiles que se pueden aplicar a casi cualquier tarea de aprendizaje: clasificación, predicción numérica e incluso reconocimiento de patrones no supervisados.

Las RNA se aplican mejor a problemas donde los datos de entrada y los datos de salida son bien entendidos o al menos bastante simples, sin embargo, el proceso que relaciona la entrada con la salida es extremadamente complejo. Como método de caja negra, funcionan bien para este tipo de problemas de caja negra.

El diagrama de red dirigida define una relación entre las señales de entrada recibidas por los nodos (variables \(x\)) y la señal de salida (variable \(y\)).

Neurona
Neurona

La señal de cada nodo se pondera (valores \(w\)) según su importancia; ignore por ahora cómo se determinan estos pesos. Las señales de entrada son sumadas por el cuerpo de la célula y la señal se transmite de acuerdo con una función de activación indicada por \(f\).

Una neurona artificial típica con \(n\) nodos de entrada puede representarse mediante la siguiente fórmula. Los pesos \(w\) permiten que cada una de las \(n\) entradas, (\(x\)), contribuya una cantidad mayor o menor a la suma de las señales de entrada. El total neto es utilizado por la función de activación \(f(x)\), y la señal resultante, \(y(x)\), es la salida.

\[ y(x) = f\left(\sum_i^nw_ix_i\right) \]

Aunque existen numerosas variantes de redes neuronales, cada una se puede definir en términos de las siguientes características:

  • Una función de activación, que transforma la señal de entrada neta de una neurona en una sola señal de salida para ser transmitida en la red

Funciones de actvación{width=60% height = 80%}

  • Una topología de red (o arquitectura), que describe el número de neuronas en el modelo, así como el número de capas y la forma en que están conectadas.

Topología de la red{width=60% height = 80%}

  • El algoritmo de entrenamiento que especifica cómo se establecen los pesos de conexión para activar las neuronas en proporción a la señal de entrada.
Fortalezas Debilidades
Se puede adaptar a problemas de clasificación o predicción numérica Reputación de ser computacionalmente intensivo y lento de entrenar, particularmente si la topología de red es compleja
Entre los enfoques de modelado más precisos Datos de entrenamiento fáciles de sobreestimar o no subestmiar
Hace pocas suposiciones sobre las relaciones subyacentes de los datos Resultados en un modelo complejo de caja negra que es difícil, si no imposible, de interpretar

11.2 Paso 1: recopilación de datos

El conjunto de datos concretos contiene \(1030\) muestras de hormigón, con ocho características que describen los componentes utilizados en la mezcla. Se cree que estas características están relacionadas con la resistencia a la compresión final e incluyen la cantidad (en kilogramos por metro cúbico) de cemento, escoria, cenizas, agua, superplastificante, agregado grueso y agregado fino utilizado en el producto, además de el tiempo de envejecimiento (medido en días).

uu <- "https://raw.githubusercontent.com/vmoprojs/DataLectures/master/concrete.csv"
concrete <- read.csv(url(uu))
# set.seed(12345)
# concrete <- concrete[order(runif(nrow(concrete))), ]
concrete <- concrete[-1]

11.3 Paso 2: Explorar y preparar los datos

str(concrete)
## 'data.frame':    1030 obs. of  9 variables:
##  $ cement      : num  141 169 250 266 155 ...
##  $ slag        : num  212 42.2 0 114 183.4 ...
##  $ ash         : num  0 124.3 95.7 0 0 ...
##  $ water       : num  204 158 187 228 193 ...
##  $ superplastic: num  0 10.8 5.5 0 9.1 0 0 6.4 0 9 ...
##  $ coarseagg   : num  972 1081 957 932 1047 ...
##  $ fineagg     : num  748 796 861 670 697 ...
##  $ age         : int  28 14 28 28 28 90 7 56 28 28 ...
##  $ strength    : num  29.9 23.5 29.2 45.9 18.3 ...

Las nueve variables en los datos corresponden a las ocho características y un resultado, aunque se ha evidenciado un problema. Las redes neuronales funcionan mejor cuando los datos de entrada se escalan a un rango estrecho alrededor de cero, y aquí vemos valores que van desde cero hasta más de mil.

Normalmente, la solución a este problema es reescalar los datos con una función de normalización o estandarización.

  • Si los datos siguen una curva en forma de campana (una distribución normal), entonces puede tener sentido utilizar la estandarización a través de la función scale () integrada de R.

  • Por otro lado, si los datos siguen una distribución uniforme o son severamente no normales, entonces la normalización a un rango de 0-1 puede ser más apropiada.

normalize <- function(x) {
return((x - min(x)) / (max(x) - min(x)))
}
concrete_norm <- as.data.frame(lapply(concrete, normalize))
summary(concrete_norm$strength)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.0000  0.2664  0.4001  0.4172  0.5457  1.0000

En comparación, los valores mínimos y máximos originales fueron 2.33 y 82.6

summary(concrete$strength)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    2.33   23.71   34.45   35.82   46.13   82.60

Cualquier transformación aplicada a los datos antes de entrenar el modelo tendrá que aplicarse en reversa más adelante para volver a las unidades de medida originales. Para facilitar el cambio de escala, es conveniente guardar los datos originales, o al menos las estadísticas de resumen de los datos originales.

Dividiremos los datos en un conjunto de capacitación con el 75 por ciento de los ejemplos y un conjunto de prueba con el 25 por ciento.

concrete_train <- concrete_norm[1:773, ]
concrete_test <- concrete_norm[774:1030, ]

11.4 Paso 3: entrenar un modelo en los datos

Para modelar la relación entre los ingredientes usados en el concreto y la resistencia del producto terminado, usaremos una red neuronal feedforward (red que no tiene ciclos) de múltiples capas. El paquete neuralnet (Fritsch and Guenther (2016)) proporciona una implementación estándar y fácil de usar de tales redes. También ofrece una función para trazar la topología de red (más paquetes aquí).

Empezaremos a entrenar la red con solo un nodo oculto

library(neuralnet)
concrete_model <- neuralnet(strength ~ cement + slag +
ash + water + superplastic +
coarseagg + fineagg + age,
data = concrete_train)

Grafiquemos el resultado

plot(concrete_model)
  • En este modelo simple, hay un nodo de entrada para cada una de las ocho características, seguido de un único nodo oculto y un único nodo de salida que predice la fuerza del concreto.

  • Los pesos para cada una de las conexiones también se representan, al igual que los términos de sesgo (indicados por los nodos con un 1).

  • El diagrama también informa el número de pasos de entrenamiento y una medida llamada, la suma de los errores cuadrados (SSE). Estas métricas serán útiles cuando estemos evaluando el rendimiento del modelo.

En ocasiones es importante estimar la complejidad del modelo cuand trabajamos con más datos:

nn_test = function(data)
{
  concrete_model <- neuralnet(strength ~ cement + slag +
ash + water + superplastic +
coarseagg + fineagg + age,
data = data)
}
# devtools::install_github("agenis/GuessCompx")
#library(GuessCompx) # get it by running: install.packages("GuessCompx")
library(neuralnet)
#CompEst(concrete_train, nn_test,plot =FALSE)

11.5 Paso 4: evaluar el rendimiento del modelo

El diagrama de topología de red nos permite observar la caja negra de la ANN, pero no proporciona mucha información sobre qué tan bien el modelo se ajusta a nuestros datos. Para estimar el rendimiento de nuestro modelo, podemos usar la función predict() para generar predicciones en el conjunto de datos de prueba:

predicted_strength <- predict(concrete_model, concrete_test[1:8])

Ten en cuenta que la función de predict() funciona de manera un poco diferente de las funciones de predict() que hemos utilizado hasta ahora. Devuelve una lista con dos componentes: $neuronas, que almacena las neuronas para cada capa en la red, y $net.results, que almacena los valores predichos. Vamos a querer lo último:

# predicted_strength <- model_results$net.result

Debido a que este es un problema de predicción numérica en lugar de un problema de clasificación, no podemos usar una matriz de confusión para examinar la precisión del modelo. En cambio, debemos medir la correlación entre nuestra fuerza del concreto predicha y el valor verdadero. Esto proporciona una idea de la fuerza de la asociación lineal entre las dos variables.

cor(predicted_strength, concrete_test$strength)
##           [,1]
## [1,] 0.8059025

No te alarmes si el resultado difiere. Debido a que la red neuronal comienza con pesos aleatorios, las predicciones pueden variar de un modelo a otro.

Si la correlación es alta implica que el modelo está haciendo un buen trabajo.

11.6 Paso 5: mejorando el ajuste

Como las redes con topologías más complejas son capaces de aprender conceptos más difíciles, veamos qué sucede cuando aumentamos la cantidad de nodos ocultos a cinco.

concrete_model2 <- neuralnet(strength ~ cement + slag +
ash + water + superplastic +
coarseagg + fineagg + age,
data = concrete_train, hidden = 5)
plot(concrete_model2)

Observa que el error informado (medido nuevamente por SSE) se ha reducido.

Comparemos los resultados:

predicted_strength2 <- predict(concrete_model2, concrete_test[1:8])
# predicted_strength2 <- model_results2$net.result
cor(predicted_strength2, concrete_test$strength)
##           [,1]
## [1,] 0.9364437
denormalize <- function(x_norm,x_orig)
{
  #x_norm: normalized dats
  #x_orig: original data
  mmin <- min(x_orig);mmax <- max(x_orig)
  denorm_val <- x_norm*(mmax-mmin)+mmin
  return(denorm_val)
}
library(caret)
R2(denormalize(predicted_strength2,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength))
##           [,1]
## [1,] 0.8769268

Parámetros más importantes

  • hidden: vector de enteros con el número de neuronas escondidas. Ejemplo, hidden=c(4,3,2) indica que se tiene tres capas ocultas con 4,3, y 2 nodos respectivamente. Por defecto es igual a 1.

  • act.fct: función de activiación. Se puede usar logistic y tanh, la primera es por defecto.

  • err.fct: función de pérdida con la que se evalúa el error cometido en las estimación durante la estimación de los pesos \(w\). Por defecto es igual a sse (errores al cuadrado).

Ajustemos un modelo con una arquitectura más compleja:

concrete_model3 <- neuralnet(strength ~ cement + slag +
ash + water + superplastic +
coarseagg + fineagg + age,
data = concrete_train, hidden = c(5,2,3),act.fct = "tanh")

plot(concrete_model3)
predicted_strength3 <- predict(concrete_model3, concrete_test[1:8])


R2(denormalize(predicted_strength3,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength))
##           [,1]
## [1,] 0.8491314

11.6.1 Usando caret

Como podemos ver, podemos variar los parámetros elegidos para elegir la mejor combinación de, por ejemplo, el número de capas y nodos. Podemos hacerlo con caret

tune.grid.neuralnet <- expand.grid(
  layer1 = 2:3,
  layer2 = 2:3,
  layer3 = 2:3
)

control <- trainControl(method="repeatedcv", number=5)

nnet_caret <- caret::train(strength ~ cement + slag + ash + water + superplastic + coarseagg + fineagg + age, 
               data = concrete_train,
               method = 'neuralnet', 
               linear.output = TRUE, 
               tuneGrid = tune.grid.neuralnet,
               metric = "RMSE",
               trControl = control)


nnet_caret$finalModel$tuneValue # valores de las capas
##   layer1 layer2 layer3
## 7      3      3      2
predicted_strength4 <- predict(nnet_caret,concrete_test[1:8])

c(m1 =R2(denormalize(predicted_strength,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength))
,
m2 = R2(denormalize(predicted_strength2,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength))
,
m3 = R2(denormalize(predicted_strength3,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength))
,
m4 = R2(denormalize(predicted_strength4,concrete$strength),
   denormalize(concrete_test$strength,concrete$strength)))
##        m1        m2        m3        m4 
## 0.6494788 0.8769268 0.8491314 0.8450435

11.6.2 Ejercicio

Usa los datos Hitters (salarios de jugadores de baseball con estadísticas de su rendimiento) cargándolos con:

library(ISLR2)
library(caret)
set.seed(8519)

# 1) Datos y partición
Gitters <- na.omit(Hitters)
depvar <- "Salary"
X_in <- Gitters[, setdiff(names(Gitters), depvar)]
y_raw <- Gitters[[depvar]]

index <- createDataPartition(y_raw, p = 0.75, list = FALSE)
train_df <- X_in[index, , drop = FALSE]
test_df  <- X_in[-index, , drop = FALSE]
y_train  <- as.numeric(scale(y_raw[index]))  # vector, no matriz
y_test   <- as.numeric(scale(y_raw[-index]))

# 2) Dummies y preprocesado (ajustado SOLO con train)
dummies <- dummyVars(~ ., data = train_df, fullRank = TRUE)
X_train_dum <- predict(dummies, newdata = train_df)
X_test_dum  <- predict(dummies, newdata = test_df)

# Quita columnas de varianza casi nula
nzv <- nearZeroVar(X_train_dum)
if (length(nzv)) {
  X_train_dum <- X_train_dum[, -nzv, drop = FALSE]
  X_test_dum  <- X_test_dum[, -nzv, drop = FALSE]
}

# Center/scale SOLO con train
pp <- preProcess(X_train_dum, method = c("center", "scale"))
X_train <- as.data.frame(predict(pp, X_train_dum))
X_test  <- as.data.frame(predict(pp, X_test_dum))

# 3) Entrenamiento con neuralnet (aumenta stepmax y usa rprop+)
tune.grid.neuralnet <- expand.grid(
  layer1 = 2:3,
  layer2 = 0:2,   # permite capas vacías (más estable)
  layer3 = 0 # solo 1–2 capas
)

ctrl <- trainControl(method = "repeatedcv", number = 5, repeats = 1)

set.seed(8519)
nnet_caret <- caret::train(
  x = X_train, y = y_train,
  method = "neuralnet",
  tuneGrid = tune.grid.neuralnet,
  trControl = ctrl,
  metric = "RMSE",
  linear.output = TRUE,
  algorithm = "rprop+",
  stepmax = 2e5,
  lifesign = "minimal"
)
## hidden: 2    thresh: 0.01    rep: 1/1    steps:   91133  error: 7.93512  time: 3.34 secs
## hidden: 3    thresh: 0.01    rep: 1/1    steps:   45601  error: 5.90109  time: 1.83 secs
## hidden: 2, 1    thresh: 0.01    rep: 1/1    steps:    4248   error: 18.47307 time: 0.2 secs
## hidden: 3, 1    thresh: 0.01    rep: 1/1    steps:   13401   error: 5.25173  time: 0.65 secs
## hidden: 2, 2    thresh: 0.01    rep: 1/1    steps: stepmax   min thresh: 0.0123381654020742
## Warning: Algorithm did not converge in 1 of 1 repetition(s) within the stepmax.
## Warning: predictions failed for Fold1.Rep1: layer1=2, layer2=2, layer3=0 Error in cbind(1, pred) %*% weights[[num_hidden_layers + 1]] : 
##   requires numeric/complex matrix/vector arguments
## hidden: 3, 2    thresh: 0.01    rep: 1/1    steps:   24070   error: 10.11108 time: 1.18 secs
## hidden: 2    thresh: 0.01    rep: 1/1    steps:   18170  error: 10.61841 time: 0.62 secs
## hidden: 3    thresh: 0.01    rep: 1/1    steps:    1451  error: 24.07298 time: 0.05 secs
## hidden: 2, 1    thresh: 0.01    rep: 1/1    steps:    5039   error: 15.05091 time: 0.22 secs
## hidden: 3, 1    thresh: 0.01    rep: 1/1    steps:   47523   error: 8.38524  time: 2.21 secs
## hidden: 2, 2    thresh: 0.01    rep: 1/1    steps:   30116   error: 17.59561 time: 1.27 secs
## hidden: 3, 2    thresh: 0.01    rep: 1/1    steps:   29069   error: 3.85045  time: 1.46 secs
## hidden: 2    thresh: 0.01    rep: 1/1    steps:    2029  error: 21.74723 time: 0.06 secs
## hidden: 3    thresh: 0.01    rep: 1/1    steps:   78015  error: 5.75582  time: 3.3 secs
## hidden: 2, 1    thresh: 0.01    rep: 1/1    steps:   27378   error: 12.32146 time: 1.21 secs
## hidden: 3, 1    thresh: 0.01    rep: 1/1    steps:    7125   error: 3.35723  time: 0.33 secs
## hidden: 2, 2    thresh: 0.01    rep: 1/1    steps:   68265   error: 6.76327  time: 3.01 secs
## hidden: 3, 2    thresh: 0.01    rep: 1/1    steps:   15877   error: 4.10099  time: 0.76 secs
## hidden: 2    thresh: 0.01    rep: 1/1    steps:   43210  error: 7.19069  time: 1.59 secs
## hidden: 3    thresh: 0.01    rep: 1/1    steps:   26663  error: 8.11192  time: 1.06 secs
## hidden: 2, 1    thresh: 0.01    rep: 1/1    steps:   12921   error: 13.43768 time: 0.55 secs
## hidden: 3, 1    thresh: 0.01    rep: 1/1    steps:    9839   error: 4.09715  time: 0.51 secs
## hidden: 2, 2    thresh: 0.01    rep: 1/1    steps: stepmax   min thresh: 0.0135449698147733
## Warning: Algorithm did not converge in 1 of 1 repetition(s) within the stepmax.
## Warning: predictions failed for Fold4.Rep1: layer1=2, layer2=2, layer3=0 Error in cbind(1, pred) %*% weights[[num_hidden_layers + 1]] : 
##   requires numeric/complex matrix/vector arguments
## hidden: 3, 2    thresh: 0.01    rep: 1/1    steps:   21009   error: 5.64973  time: 1.03 secs
## hidden: 2    thresh: 0.01    rep: 1/1    steps:    8030  error: 16.75476 time: 0.28 secs
## hidden: 3    thresh: 0.01    rep: 1/1    steps:   12082  error: 9.17427  time: 0.51 secs
## hidden: 2, 1    thresh: 0.01    rep: 1/1    steps:   10755   error: 17.51094 time: 0.43 secs
## hidden: 3, 1    thresh: 0.01    rep: 1/1    steps:   15038   error: 4.02102  time: 0.74 secs
## hidden: 2, 2    thresh: 0.01    rep: 1/1    steps: stepmax   min thresh: 0.0209678873158622
## Warning: Algorithm did not converge in 1 of 1 repetition(s) within the stepmax.
## Warning: predictions failed for Fold5.Rep1: layer1=2, layer2=2, layer3=0 Error in cbind(1, pred) %*% weights[[num_hidden_layers + 1]] : 
##   requires numeric/complex matrix/vector arguments
## hidden: 3, 2    thresh: 0.01    rep: 1/1    steps:   21819   error: 10.52351 time: 1.09 secs
## Warning in nominalTrainWorkflow(x = x, y = y, wts = weights, info = trainInfo, : There were
## missing values in resampled performance measures.
## hidden: 3    thresh: 0.01    rep: 1/1    steps:    5549  error: 22.05278 time: 0.25 secs
nnet_caret
## Neural Network 
## 
## 200 samples
##  19 predictor
## 
## No pre-processing
## Resampling: Cross-Validated (5 fold, repeated 1 times) 
## Summary of sample sizes: 160, 160, 161, 159, 160 
## Resampling results across tuning parameters:
## 
##   layer1  layer2  RMSE       Rsquared   MAE      
##   2       0       0.7158934  0.4777751  0.5470894
##   2       1       0.7982210  0.4647982  0.5453739
##   2       2       0.8337062  0.5028931  0.5577040
##   3       0       0.6861893  0.5651652  0.4828159
##   3       1       0.9920416  0.4133856  0.6240881
##   3       2       0.9044428  0.4639449  0.6066114
## 
## Tuning parameter 'layer3' was held constant at a value of 0
## RMSE was used to select the optimal model using the smallest value.
## The final values used for the model were layer1 = 3, layer2 = 0 and layer3 = 0.

Se predice su salario usando regresión lineal múltiple y neuralnet. ¿Cuál es mejor? ¿Por qué?

11.7 Redes neuronales convolucionales

Conjunto de datos de 50.000 imágenes de entrenamiento en color de 32x32, etiquetadas en más de 100 categorías y 10.000 imágenes de prueba.

Son 20 clases de mamíferos (ejemplo: acuáticos), con 5 tipos (castor, delfín, nutria, foca, ballena), 100 etiquetas en total.

Nota: el siguiente enlace tiene las instrucciones para instalar tensorflow y keras: https://tensorflow.rstudio.com/install/

# install.packages("keras")
# install_keras()
# library(tensorflow)
# install_tensorflow()
# https://tensorflow.rstudio.com/install/
library(keras3)

cifar100 <- dataset_cifar100()  # Descarga/carga CIFAR-100 ya particionado en train/test
names(cifar100)                 # Estructura: lista con $train y $test
## [1] "train" "test"
table(cifar100$train$y)         # Frecuencias por clase (0–99), útil para ver balanceo
## 
##   0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20  21  22  23 
## 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 
##  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47 
## 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 
##  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71 
## 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 
##  72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95 
## 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 500 
##  96  97  98  99 
## 500 500 500 500
x_train <- cifar100$train$x     # Tensores de imágenes: [n, 32, 32, 3]
g_train <- cifar100$train$y     # Etiquetas enteras (0–99)
x_test  <- cifar100$test$x
g_test  <- cifar100$test$y

dim(x_train)                    # 50000 x 32 x 32 x 3
## [1] 50000    32    32     3
range(x_train[1,,, 1])          # Rango de intensidades (canal 1) antes de normalizar
## [1]  13 255
one_hot_encode <- function(class_vector, num_classes = NULL) {
  class_vector <- as.integer(class_vector)      # Asegura enteros 0..K-1
  if (is.null(num_classes)) {
    num_classes <- max(class_vector) + 1        # Si no dan K, se infiere K = max + 1
  }
  binary_matrix <- matrix(0, nrow = length(class_vector), ncol = num_classes)
  for (i in seq_along(class_vector)) {
    binary_matrix[i, class_vector[i] + 1] <- 1 # +1: R indexa desde 1
  }
  return(binary_matrix)
}

x_train <- x_train / 255        # Normaliza intensidades a [0,1] (mejora estabilidad)
x_test  <- x_test  / 255
y_train <- one_hot_encode(g_train, num_classes = 100)  # One-hot para 100 clases
dim(y_train)
## [1] 50000   100

Atención: - num_classes = 100: fija la dimensión de salida de one-hot (columnas = clases).

  • Normalizar a [0,1] ayuda a que el optimizador converja mejor.

El conjunto de 50.000 imágenes de formación tiene cuatro dimensiones:

  • Cada imagen de tres colores se representa como un conjunto de tres canales, cada uno de los cuales consta de \(32\times 32\) píxeles de ocho bits.

Estandarizamos, pero manteniendo la estructura de la matriz. Codificamos la respuesta one-hot para producir una matriz binaria de 100 columnas.

Antes de comenzar, miramos algunas de las imágenes de entrenamiento usando el paquete jpeg

library(jpeg)                               # (opcional) utilidades de imagen
par(mar = c(0, 0, 0, 0), mfrow = c(5, 5))  # mar: márgenes; mfrow: 25 paneles (5x5)
index <- sample(seq(50000), 25)            # 25 índices aleatorios del set de train
for (i in index) plot(as.raster(x_train[i,,, ]))  # Renderiza 32x32x3 como imagen

La función as.raster() convierte el mapa de características (x_train) para que pueda trazarse como una imagen en color.

Aquí especificamos una CNN de tamaño moderado con fines de demostración.

# API Funcional (recomendada en keras3)
inputs <- layer_input(shape = c(32, 32, 3))
# shape: forma de una sola imagen (alto, ancho, canales). No incluye tamaño de batch.

outputs <- inputs %>%
  layer_conv_2d(32, c(3,3), padding = "same", activation = "relu") %>%
  # 32 filtros; kernel 3x3; padding "same" conserva tamaño espacial; ReLU como activación.

  layer_max_pooling_2d(c(2,2)) %>%
  # Pooling 2x2 reduce a la mitad alto/ancho.

  layer_conv_2d(64, c(3,3), padding = "same", activation = "relu") %>%
  layer_max_pooling_2d(c(2,2)) %>%
  layer_conv_2d(128, c(3,3), padding = "same", activation = "relu") %>%
  layer_max_pooling_2d(c(2,2)) %>%
  layer_conv_2d(256, c(3,3), padding = "same", activation = "relu") %>%
  layer_max_pooling_2d(c(2,2)) %>%
  layer_flatten() %>%                       # Aplana [H, W, C] -> vector 1D
  layer_dense(512, activation = "relu") %>% # Capa densa con 512 neuronas (no linealidad ReLU)
  layer_dropout(0.5) %>%                    # 50% de “apagado” (regularización)
  layer_dense(100, activation = "softmax")  # Salida: 100 clases con softmax (probabilidades)

model <- keras_model(inputs, outputs)

model %>% compile(
  optimizer = optimizer_adam(learning_rate = 1e-3),
  # optimizer_adam: regla de actualización; learning_rate = 0.001 (paso inicial)

  loss = "categorical_crossentropy",
  # Pérdida para clasificación multiclase con labels one-hot

  metrics = "accuracy"
  # Reporta accuracy en entrenamiento/validación
)

Parámetros de capas: - layer_conv_2d(filters, kernel_size, padding, activation): #filtros, tamaño kernel, borde y no linealidad.

  • layer_max_pooling_2d(pool_size): submuestreo espacial (reduce resolución).

  • layer_dense(units, activation): neuronas y activación (ReLU/softmax).

  • layer_dropout(rate): proporción de neuronas “apagadas” para evitar overfitting.

  • optimizer_adam(learning_rate = 1e-3): tamaño de paso del optimizador (ajustable).

Finalmente, especificamos el algoritmo de ajuste y ajustamos el modelo.

library(caret)   # (opcional) utilidades de ML clásicas; no es requerido por Keras
library(ISLR2)   # (opcional) datasets de ejemplo; no se usa aquí

# Puedes recompilar con otro optimizador para comparar (opcional):
model %>% compile(loss = "categorical_crossentropy",
                  optimizer = optimizer_rmsprop(),  # RMSprop: paso adaptativo por dimensión
                  metrics = c("accuracy"))

# history <- model %>% fit(x_train, y_train, epochs = 30,
history <- model %>% fit(
  x_train, y_train,
  epochs = 1,                # Nº de pasadas completas por el set de entrenamiento
  batch_size = 128,          # Tamaño del mini-lote (trade-off ruido/uso de memoria)
  validation_split = 0.20    # Toma 20% del train para validación interna
)
  • epochs: más épocas \(\approx\) mayor tiempo/posible mejor ajuste (vigilar overfitting).

  • batch_size: 128 suele equilibrar velocidad/estabilidad; ajusta según GPU/CPU.

  • validation_split = 0.20: separa aleatoriamente 20% de x_train/y_train para validar.

Nota: si quieres validar en test explícito, usa validation_data = list(x_test, y_test) en lugar de validation_split.

sol <- model %>% predict(x_test)                   # Probabilidades por clase [n_test x 100]
pred_clases <- apply(sol, 1, which.max) - 1        # Índice de argmax por fila; -1 para 0..99

# Accuracy simple (usar length(g_test), no nrow())
acc_test <- mean(pred_clases == as.integer(g_test))
acc_test
## [1] 0.0966
  • predict(x_test): devuelve probabilidades por clase.

  • apply(..., 1, which.max): “argmax” por fila (imagen).

  • - 1: ajusta de índice 1-based (R) a etiquetas 0-based (CIFAR-100).

  • Corrección: usa length(g_test) o mean(...) para accuracy (no nrow(g_test) porque g_test es vector).