En el capítulo 2, tocamos brevemente el tema de redes neuronales en nuestra exploración del panorama del Machine Learning.
Una red neuronal es un conjunto de ecuaciones que se usan para calcular un resultado. Estas ecuaciones no son muy aterradoras sí las pensamos como un cerebro hecho de códigos de computadora. En algunos casos, esto está más cerca a la realidad de lo que deberíamos esperar de un ejemplo tan sencillo. Dependiendo del número de características que se tengan en los datos, la red neuronal puede ser una “caja negra”. En principio, se pueden mostrar las ecuaciones que hacen a la red neuronal, pero en cierta medida, la cantidad de información se vuelve demasiado engorrosa para ser intuida fácilmente.
Las redes neuronales son usadas ampliamente en las grandes industrias debido a su precisión. Algunas veces, hay que escoger un modelo bastante preciso pero con una velocidad de cálculos muy lenta. Por lo tanto, lo mejor es intentar varios modelos y usar redes neuronales solo si ellas trabajan bien para un conjunto de datos en particular.
En el Capítulo 2, se observó el desarrollo de una puerta \(AND\). Una puerta \(AND\) tiene la siguiente lógica.
x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic
## x1 x2 AND
## 1 0 0 0
## 2 0 1 0
## 3 1 0 0
## 4 1 1 1
Si se tienen dos entradas \(1\) (ambas verdaderas), la salida es \(1\) (verdadero). Además, si algunas de ellas, o ambas son \(0\) (falso) la salida es también \(0\) (falso). Estos cálculos son similares en nuestro análisis de la regresión logística.
En el Capítulo 4, se observo como la función sigmoide funciona. Recordemos que la función sigmoide esta dada por:
\(g(z) = \frac{1}{1+e^{-z}}\)
Donde \(z\) es una función de la forma \(z = \theta_0 + \theta_1 x_1 + \theta_2 x_2\)
Para la puerta lógica, todo lo que se necesita es tomar y escoger los pesos \(\theta_0,\theta_1,\theta_2\) para que cuando \(x_1 = 1, x_2 = 1\) los resultados de \(z\) cuando se pasen a través de la función sigmoide \(g(z)\) también sea \(1\). Previamente, se escogieron los pesos de \(\theta_0 =20, \theta_1 = 15, \theta_2 = 17\) para satisfacer la ecuación. La manera en la que la red neuronal calcula estos pesos es un proceso más matemático, pero sigue la misma lógica que se usó para la regresión logística.
Las redes neuronales vienen de diferentes “sabores y colores”, pero las más populares se derivan de las redes neuronales de una capa o multicapa. Hasta ahora, se observó un ejemplo de una red de una sola capa, para la que se tomó una entrada \((1,0)\), se procesó a través de la función sigmoide y se obtuvo una salida \((0)\). Se puede, de hecho hacer una cadena de estos cálculos para formar modelos interconectados y complejos tomando la salida de uno y pasandolo a otras capas computacionales.
set.seed(123)
AND <- c(rep(0, 3), 1) #rep(0,3)=(0 0 0)
print(AND)
## [1] 0 0 0 1
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1)), AND) #expand.grid todas las combinaciones de los vectores
net <- neuralnet(AND ~ Var1 + Var2, binary.data, hidden = 0,
err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figura 5-1.Una red neuronal simple
Antes de comenzar con las matemáticas, vamos a desglosar la visualización presentada en la Figura 5-1, un diagrama de una red neuronal más sencilla que se puede hacer. Hay una capa de entrada (los círculos vacíos a la izquierda) y una capa de salida (el círculo vacío a la derecha). A menudo, hay otra capa vertical de círculos que indica una capa de cálculos. En este caso, la capa de salida es la capa de cálculos. Los números en las líneas indican los mejores pesos (computacionalmente hablando) para usar en el modelo. El número que acompaña a el “\(1\)” en el círculo ubicado en la parte superior es el peso del nodo de sesgo. El nodo de sesgo no es más que la constante aditiva para la ahora familiar función sigmoide que se usó en los ejemplos de la regresión logística. Entonces, en cierto sentido, esto es sólo una forma diferente de representar un análisis de regresión logística en la forma más simple de una red neuronal. El resultado final es un esquema de clasificación para los datos que tienen etiquetas \(1\) o \(0\).
En R, hay solo una librería de redes neuronales que tiene la funcionalidad para visualizar redes neuronales. En la práctica, la mayoría del tiempo graficar redes neuronales es más complicado de lo que parece, como se verá después. En escenarios de modelos complejos, los diagramas de redes neuronales y las matemáticas se vuelven muy engorrosas hasta el punto de que el modelo en sí mismo se vuelve una caja negra entrenada. Sí el gerente nos pide que le expliquemos las matemáticas detrás de un modelo de red neuronal compleja, puede que necesitemos de una tarde completa y el tablero más grande en el edificio.
El código mostrado en la Figura 5-1 muestra una tabla similar a los datos de binary.data
en la función neuralnet()
(función del paquete del mismo nombre). El resultado que se obtiene será una ecuación que tiene los pesos \(\theta_0 = -11.86048, \theta_1 = 7.75382, \theta_2 = 7.75519\).
Entonces, si nuestro jefe quiere realmente saber el estado del proceso del modelo de la red neuronal, se podrá decir que el modelo está listo para funcionar. No obstante, si nuestro jefe nos pide detalles de cómo funciona exactamente, puedemos decir que se toman dos entradas \(Var_1\) y \(Var_2\) y se introduce en la ecuación:
\[z = -11.86048 + 7.75382 Var_1 + 7.75519 Var_2\]
Entonces pasamos la ecuación a través de la función sigmoide \(g(z) = \frac{1}{1+e^{-z}}\) y obtenemos la salida. Entonces, el proceso entero será de la siguiente manera:
\[AND = \frac{1}{1+ e^{-(-11.86048 + 7.75382 Var_1 + 7.75519 Var_2)}}\]
Se puede verificar la salida de la función neuralnet()
usando la función prediction()
prediction(net)
## Data Error: 0;
## $rep1
## Var1 Var2 AND
## 1 0 0 7.064116e-06
## 2 1 0 1.619615e-02
## 3 0 1 1.621788e-02
## 4 1 1 9.746310e-01
##
## $data
## Var1 Var2 AND
## 1 0 0 0
## 2 1 0 0
## 3 0 1 0
## 4 1 1 1
En la primera tabla están las variables de entrada y lo que la red neuronal piensa que es la respuesta. Como puede ver, las respuestas están bastante cerca de lo que deberían ser, que es lo que se muestra en la tabla de abajo. Hasta ahora, se ha realizado con éxito una red neuronal con una sola capa. Es decir, todas las entradas pasaron por un solo punto de procesamiento.
como se muestra en la Figura 5-1. Estos puntos de procesamiento son casi siempre funciones sigmoides, sin embargo, en algunos casos inusuales, se pueden pasar por la función tangente hiperbólica \(\tanh(x)\), para conseguir un resultado similar.
Como se ha mencionado anteriormente, las redes neuronales pueden tomar múltiples entradas y proporcionar múltiples salidas. Por ejemplo, se tienen dos funciones que se desean modelar mediante redes neuronales, se pueden usar los operadores de formula en R ~
y el operador +
para añadir otra respuesta al lado izquierdo de la ecuación durante el modelado, tal como se ve en la Figura 5-2
set.seed(123)
AND <- c(rep(0, 7), 1)
OR <- c(0, rep(1, 7))
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1), c(0,
1)), AND, OR)
net <- neuralnet(AND + OR ~ Var1 + Var2 + Var3, binary.data,
hidden = 0, err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figura 5-2.Las redes neuronales pueden ser mas complicadas que la regresión logística en términos de las multiples salidas de cálculo
Se pueden modelar las funciones \(AND\) y \(OR\) con dos ecuaciones dadas por las salidas en la Figura 5-2
\[\begin{align*} AND &=g( -18.4 + 7.2 \cdot Var_1 + 7.2 \cdot Var_2 + 7.1 \cdot Var_3) \\ OR &=g( -9.53 + 22.5\cdot Var_1 + 21.2 \cdot Var_2 + 19.5 \cdot Var_3 ) \end{align*}\]
Podemos observar que la salida es similar al proceso anterior con una sola función:
prediction(net)
## Data Error: 0;
## $rep1
## Var1 Var2 Var3 AND OR
## 1 0 0 0 1.045615e-08 7.220621e-05
## 2 1 0 0 1.426236e-05 9.999977e-01
## 3 0 1 0 1.409371e-05 9.999919e-01
## 4 1 1 0 1.886199e-02 1.000000e+00
## 5 0 0 1 1.228339e-05 9.999546e-01
## 6 1 0 1 1.647909e-02 1.000000e+00
## 7 0 1 1 1.628740e-02 1.000000e+00
## 8 1 1 1 9.575992e-01 1.000000e+00
##
## $data
## Var1 Var2 Var3 AND OR
## 1 0 0 0 0 0
## 2 1 0 0 0 1
## 3 0 1 0 0 1
## 4 1 1 0 0 1
## 5 0 0 1 0 1
## 6 1 0 1 0 1
## 7 0 1 1 0 1
## 8 1 1 1 1 1
La redes neuronales parecen funcionar bien.
Hasta ahora se han construido redes neuronales que no tienen capas ocultas. Es decir, la capa de cálculo es la misma que la capa de salida. La Red Neuronal de la Figura 5-3 se compone de una capa oculta y una capa de salida. Aquí, mostraremos cómo al añadir una capa oculta de cálculos puede ayudar a incrementar la precisión del modelo.
Las redes neuronales usan notaciones abreviadas para definir su arquitectura, en la que anotaremos el número de nodos de entrada, seguido de dos puntos, el número de nodos de cálculo en la capa oculta, otros dos puntos y luego el número de nodos de salida. La arquitectura de las redes neuronales que construimos en la Figura 5-3 tendrían una notación de \(3:1:1\)
La Figura 5-3 tiene tres entradas, una capa oculta y una capa de salida para una arquitectura de red neuronal \(3:1:1\).
set.seed(123)
AND <- c(rep(0, 7), 1)
binary.data <- data.frame(expand.grid(c(0, 1), c(0, 1), c(0,
1)), AND, OR)
net <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data, hidden = 1,
err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figura 5-3. Una red neuronal con tres entradas, una capa oculta y una capa de salida (3:1:1)
En este caso, se introdujo un paso de cálculos antes de la salida. Caminando a través del diagrama de izquierda a derecha, hay tres entradas para una puerta lógica. Estos datos se introducen en la función de regresión logística en la capa oculta central. La ecuación resultante se canaliza a la capa de cálculos para que se utilice en la función \(AND\). Matemáticamente se ve de la siguiente manera:
\[\begin{equation*} H_1 = 8.57 -3.5 \cdot Var_1 -3.5 \cdot Var_2 -3.6 \cdot Var_3 \end{equation*}\]
Que luego pasaremos a través de la función logística. Entonces:
\[\begin{equation*} g(H_1) = \frac{1}{1+e^{-(8.57 - 3.5 \cdot Var_1 -3.5 \cdot Var_2 -3.6 \cdot Var_3)}} \end{equation*}\]
Por lo tanto, tomamos la anterior salida y la evaluamos en otro nodo de regresión logística usando los pesos calculados en el nodo de salida:
\[\begin{equation*} AND = g(5.72 - 13.79\cdot g(H_1)) \end{equation*}\]
Una de las mayores ventajas de usar una capa oculta con algún nodo oculto de cálculos es que hace la red neuronal más precisa. Además, entre más compleja se haga la red neuronal, será más lenta y difícil de explicar con ecuaciones intuitivas. Por otra parte, entre más capas ocultas de cálculos se tenga, se puede correr el riesgo de que se tenga un sobreajuste en el modelo, tal como se vio en los sistemas tradicionales de regresión.
Aunque los números están ligados a los pesos de cada nodo de cálculo como se ve en la Figura 5-4, estos se vuelven bastante ilegibles, lo más importante aquí es el error y el número de cálculos. En este caso el error ha bajado de \(0.033\) a \(0.026\) desde el último modelo, pero además también se redujo el número de cálculos para obtener esa precisión de \(143\) a \(61\). Por lo tanto, no sólo ha aumentado la precisión, sino además ha hecho más rápido el cálculo del modelo. La figura 5-4 también muestra otro nodo de cálculo oculto añadido a la capa oculta con un único nodo, justo antes de la capa de salida:
#figura 5-4
set.seed(123)
net2 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data, hidden = 2, err.fct = "ce", linear.output = FALSE)
plot(net2, rep = "best")
Figura 5-4. Acá se visualiza una arquitectura de una red neuronal 3:4:1
Matemáticamente, esto puede ser representado por dos ecuaciones de regresión logística que son introducidas en una ecuación de regresión logística final para obtener nuestra salida.
\[\begin{align*} H_1 &= 13.64 + 13.97\cdot Var_1 + 14.9\cdot Var_2 + 14.27\cdot Var_3 \\ H_2 &= -7.95 + 3.24\cdot Var_1 + 3.15\cdot Var_2 + 3.29\cdot Var_3 \\ H_3 &= -5.83 - 1.94\cdot g(H_1) + 14.09\cdot g(H_2) \\ AND &= g(H_3) \end{align*}\]
Las ecuaciones se vuelven cada vez más complicadas con cada aumento en el número de nodos informáticos ocultos. El error con dos nodos aumentó ligeramente de \(0.29\) a \(0.33\), pero el número de pasos de iteración que tomó el modelo para minimizar ese error fue un poco mejor, ya que bajó de \(156\) a \(143\). ¿Qué sucede si aumenta aún más el número de nodos de cálculo? Las figuras 5-5 y 5-6 ilustran esto.
set.seed(123)
net4 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data, hidden = 4,
err.fct = "ce", linear.output = FALSE)
plot(net4, rep = "best")
Figura 5-5. Una red neuronal con cuatro nodos de cómputo en una sola capa oculta
net8 <- neuralnet(AND ~ Var1 + Var2 + Var3, binary.data, hidden = 8,
err.fct = "ce", linear.output = FALSE)
plot(net8, rep = "best")
Figura 5-6. Un modelo de red neuronal 3:8:1 con sobre ajuste
El código de las Figuras 5-5 y 5-6 utiliza el mismo escenario de modelado de redes neuronales, pero el número de nodos de cálculo ocultos aumenta primero a cuatro y luego a ocho. La red neuronal con cuatro nodos de cálculo ocultos tuvo un mejor nivel de error (leve cambio) que la red con un solo nodo oculto. En ese caso, el error bajó de \(0,029\) a \(0,028\), pero el número de pasos se redujo drásticamente de \(156\) a \(61\). ¡Una gran mejora! Sin embargo, una red neuronal con ocho capas de cálculo ocultas podría haber cruzado un territorio de sobreajuste. En esa red, el error pasó de \(0,029\) a \(0,051\), aunque el número de pasos pasó de \(156\) a \(48\).
También puede aplicar la misma metodología con múltiples resultados, aunque la trama en sí comienza a convertirse en un desastre ilegible en algún momento, como lo demuestra la Figura 5-7:
#figura 5-7
set.seed(123)
net <- neuralnet(AND + OR ~ Var1 + Var2 + Var3, binary.data,
hidden = 6, err.fct = "ce", linear.output = FALSE)
plot(net, rep = "best")
Figura 5-7. También podemos diseñar redes neuronales para tener múltiples salidas de cómputos, aún teniendo múltiples nodos de cómputo en la capa oculta
Hasta ahora, todas las redes neuronales con las que hemos jugado han tenido una arquitectura que tiene una capa de entrada, una o cero capas ocultas (o capas de cómputo) y una capa de salida.
Ya hemos utilizado redes neuronales \(1:1:1\) o \(1:0:1\) para algunos esquemas de clasificación. En esos ejemplos, estábamos tratando de modelar clasificaciones basadas en las funciones de puerta lógica \(AND\) y \(OR\):
x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic$OR <- as.numeric(x1 | x2)
logic
## x1 x2 AND OR
## 1 0 0 0 0
## 2 0 1 0 1
## 3 1 0 0 1
## 4 1 1 1 1
Como muestra la Figura 5-8, podemos representar esta tabla como dos gráficos, uno de los cuales evidencia los valores de entrada y los encierra de acuerdo con el tipo de salida de puerta lógica que usamos:
#figura 5-8
logic$AND <- as.numeric(x1 & x2) + 1
logic$OR <- as.numeric(x1 | x2) + 1
par(mfrow = c(2, 1))
plot(x = logic$x1, y = logic$x2, pch = logic$AND, cex = 2,
main = "Clasificación simple de dos tipos",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim = c(-0.5,
1.5))
plot(x = logic$x1, y = logic$x2, pch = logic$OR, cex = 2,
main = "Clasificación simple de dos tipos",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim = c(-0.5,
1.5))
Figura 5-8. En estos dos casos de clasificación, podemos separar en dos clases mediante un límite de decisión en línea recta
Estos gráficos usan triángulos para indicar cuando las salidas son \(1\) (VERDADERO), y círculos para los que las salidas son \(0\) (FALSO). En nuestra discusión sobre regresión logística, básicamente estábamos encontrando algún tipo de línea que separara estos datos en puntos rojos en un lado y puntos negros en el otro. Recuerde que esta línea de separación se llama límite de decisión y siempre ha sido una línea recta. Sin embargo, no podemos usar una línea recta para intentar clasificar puertas lógicas más complicadas como \(XOR\) o \(XNOR\).
En forma tabular, como se ha visto con las funciones \(AND\) y \(OR\), las funciones \(XOR\) y \(XNOR\) toman entradas de \(x1\), \(x2\) y dan una salida numérica de la misma manera, como se muestra en la Figura* 5-9:
x1 <- c(0, 0, 1, 1)
x2 <- c(0, 1, 0, 1)
logic <- data.frame(x1, x2)
logic$AND <- as.numeric(x1 & x2)
logic$OR <- as.numeric(x1 | x2)
logic$XOR <- as.numeric(xor(x1, x2))
logic$XNOR <- as.numeric(x1 == x2)
logic
## x1 x2 AND OR XOR XNOR
## 1 0 0 0 0 0 1
## 2 0 1 0 1 1 0
## 3 1 0 0 1 1 0
## 4 1 1 1 1 0 1
logic$XOR <- as.numeric(xor(x1, x2)) + 1
logic$XNOR <- as.numeric(x1 == x2) + 1
par(mfrow = c(2, 1))
plot(x = logic$x1, y = logic$x2, pch = logic$XOR, cex = 2, main = "Clasificación de dos tipos no lineal",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim = c(-0.5,
1.5))
plot(x = logic$x1, y = logic$x2, pch = logic$XNOR, cex = 2, main = "Clasificación de dos tipos no lineal",
xlab = "x", ylab = "y", xlim = c(-0.5, 1.5), ylim = c(-0.5,
1.5))
Figura 5-9. En estos dos casos, ninguna línea recta puede separar las dos clases; sin embargo, múltiples líneas rectas combinadas pueden formar una curva que se puede usar como límite de decisión no lineal para separar los datos en clases.
No hay una sola línea recta que pueda separar los triángulos y círculos en los gráficos de la Figura 5-9. Si intenta trazar una red neuronal muy simple sin capas ocultas para una clasificación \(XOR\), los resultados no son especialmente gratificantes, como se ilustra en la Figura 5-10:
logic$XOR <- as.numeric(xor(x1, x2))
set.seed(123)
net.xor <- neuralnet(XOR ~ x1 + x2, logic, hidden = 0, err.fct = "ce",
linear.output = FALSE)
prediction(net.xor)
## Data Error: 0;
## $rep1
## x1 x2 XOR
## 1 0 0 0.4870313
## 2 1 0 0.4970851
## 3 0 1 0.4980805
## 4 1 1 0.5081364
##
## $data
## x1 x2 XOR
## 1 0 0 0
## 2 1 0 1
## 3 0 1 1
## 4 1 1 0
#figura 5-10
plot(net.xor, rep = "best")
Figura 5-10. Calcular una salida no lineal con una sola capa oculta (en este caso, la capa oculta es la capa de cálculo) produce grandes errores
Intentar usar una red neuronal sin capas ocultas es un gran error. Al observar el resultado de la función de prediction()
, puede ver que la red neuronal piensa que para un escenario dado, como \(xor(0,0)\), la respuesta es \(0.48\). Tener un error que es mucho más alto que el nivel de granularidad para el que está tratando de encontrar la respuesta indica que este no es el mejor método que puede usar.
En lugar del enfoque tradicional de usar una o ninguna capa oculta, que proporciona un límite de decisión en línea recta, debe confiar en límites de decisión no lineales, o curvas, para separar clases de datos. Al agregar más capas ocultas a sus redes neuronales, agrega más límites de decisión de regresión logística como líneas rectas. A partir de estas líneas agregadas, puede dibujar un límite de decisión convexo que habilita la no linealidad. Para ello, debe confiar en una clase de redes neuronales llamadas perceptrones multicapa o MLP.
Una forma “rápida y poco detallada” (forma sencilla de solucionar un problema) de usar un MLP en este caso sería usar las entradas \(x1\) y \(x2\) para obtener las salidas de las funciones \(AND\) y \(OR\). Luego, puede alimentar esas salidas como entradas individuales en una red neuronal de una sola capa, como se ilustra en la Figura 5-11:
set.seed(123)
and.net <- neuralnet(AND ~ x1 + x2, logic, hidden = 2, err.fct = "ce",
linear.output = FALSE)
and.result <- data.frame(prediction(and.net)$rep1)
## Data Error: 0;
or.net <- neuralnet(OR ~ x1 + x2, logic, hidden = 2, err.fct = "ce",
linear.output = FALSE)
or.result <- data.frame(prediction(or.net)$rep1)
## Data Error: 0;
as.numeric(xor(round(and.result$AND), round(or.result$OR)))
## [1] 0 1 1 0
xor.data <- data.frame(and.result$AND, or.result$OR,
as.numeric(xor(round(and.result$AND),
round(or.result$OR))))
names(xor.data) <- c("AND", "OR", "XOR")
xor.net <- neuralnet(XOR ~ AND + OR, data = xor.data, hidden = 0,
err.fct = "ce", linear.output = FALSE)
prediction(xor.net)
## Data Error: 0;
## $rep1
## AND OR XOR
## 1 0.0001754982 0.01115157 0.013427053
## 2 0.0021855081 0.99537740 0.993710673
## 3 0.0080918285 0.99566428 0.993306664
## 4 0.9853433841 0.99806092 0.003024048
##
## $data
## AND OR XOR
## 1 0.0001754982 0.01115157 0
## 2 0.0021855081 0.99537740 1
## 3 0.0080918285 0.99566428 1
## 4 0.9853433841 0.99806092 0
#figura 5-11
plot(xor.net, rep = "best")
Figura 5-11. Puede sortear las limitaciones del algoritmo calculado primero una capa única y luego pasando los resultados a otra capa única y luego pasar los resultados a otra capa única de cálculo para emular una red neuronal multicapa
Un MLP es exactamente lo que su nombre implica. Un perceptrón es un tipo particular de red neuronal que implica una forma específica de cómo calcula los pesos y los errores, conocida como red neuronal de retroalimentación. Al tomar ese principio y agregar múltiples capas ocultas, lo hacemos compatible con datos no lineales como el que estamos tratando en una puerta XOR.
Se Han analizado algunos ejemplos exhaustivos que demuestran cómo se pueden usar redes neuronales para construir sistemas como puertas \(AND\) y \(OR\), cuyas salidas luego se pueden combinar para formar cosas como puertas \(XOR\). Las redes neuronales son adecuadas para modelar funciones simples, pero cuando las encadena, a veces necesita confiar en fenómenos más complejos como MLP.
Se puede utilizar redes neuronales para problemas estándar de Machine Learning como regresión y clasificación. Para caminar suavemente a través del uso de redes neuronales para regresión, véase la figura 5-12, en donde se representa un ejemplo simple con un caso familiar de regresión lineal, por tanto se tiene una buena base lineal de entendimiento. Por ejemplo, al usar el conjunto de datos BostonHousinhg
de la librería mlbench
:
library(mlbench)
## Warning: package 'mlbench' was built under R version 4.0.4
data(BostonHousing)
lm.fit <- lm(medv ~ ., data = BostonHousing)
lm.predict <- predict(lm.fit)
plot(BostonHousing$medv, lm.predict, main = "Predicción de la regresión lineal vs
actual",
xlab = "Actual", ylab = "Predicción")
Figura 5-12. Una foma de medir el desempeño de un modelo es comparando las salidas de la predicción con lo que realmente son
Esto crea un modelo lineal de medv
, la mediana de casas dueño-ocupado en miles de dólares. Luego, la función predict()
itera sobre todas las entradas del conjunto de datos usando el modelo que ya se creó y se almacenan las predicciones. Las predicciones son graficadas versus el valor actual. En un caso ideal de un modelo perfecto, la gráfica resultante va a ser una perfecta relación lineal de \(y=x\).
Entonces, ¿cómo se compara la regresión de la red neuronal? la Figura 5-13 muestra como:
library(nnet)
## Warning: package 'nnet' was built under R version 4.0.4
nnet.fit1 <- nnet(medv ~ ., data = BostonHousing, size = 2)
## # weights: 31
## initial value 283985.903126
## final value 277329.140000
## converged
## # weights: 31
## initial value 283985.903126
## final value 277329.140000
## converged
nnet.predict1 <- predict(nnet.fit1)
plot(BostonHousing$medv, nnet.predict1, main = "Predicción red neuronal vs
actual",
xlab = "Actual", ylab = "Predicción")
Figure 5-13. Algo a tener en cuenta al cambiar de modelo es la necesidad de normalizar los datos primero
De acuerdo con el ajuste de red neuronal con dos nodos ocultos en una capa computacional oculta, el resultado es de hecho aun más terrible. Esto debe garantizar alguna investigación profunda. Démosle una mirada a la respuesta:
summary(BostonHousing$medv)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 5.00 17.02 21.20 22.53 25.00 50.00
El rango de la respuesta es de \(5\) a \(50\). Las redes neuronales no son muy buenas usando números que varían demasiado, se necesita emplear una técnica conocida como escala de característica. La escala de característica es la práctica de normalizar los datos a valores entre \(0\) y \(1\), de forma que se puedan ajustar a un cierto modelo de machine learning para un resultado más preciso. En este caso, se quiere dividir la respuesta en \(50\) para normalizar los datos:
summary(BostonHousing$medv/50)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.1000 0.3405 0.4240 0.4507 0.5000 1.0000
Ahora, se tiene una respuesta con un mínimo de \(0.1\) y un máximo de \(1\). La figura 5-14 muestra cómo esto afecta el modelo de redes neuronales:
nnet.fit2 <- nnet(medv/50 ~ ., data = BostonHousing, size = 2,
maxit = 1000, trace = FALSE)
nnet.predict2 <- predict(nnet.fit2) * 50
plot(BostonHousing$medv, nnet.predict2, main = "Predicción de red neuronal vs
actual con respuestas de salida normalizadas",
xlab = "Actual", ylab = "Predicción")
Figura 5-14. Un modelo de red neuronal con los datos normalizados adecuadamente
Este es el resultado de un modelo de red neuronal con sus entradas normalizadas correctamente.
Esta gráfica se ve un poco mejor que la anterior, pero es aun mejor para cuantificar la diferencia entre dos escenarios de modelado. Se puede hacer esto al mirar el error cuadrático medio:
mean((lm.predict - BostonHousing$medv)^2)
## [1] 21.89483
mean((nnet.predict2 - BostonHousing$medv)^2)
## [1] 16.1287
El error total para el modelo lineal es alrededor de \(22\), mientras que el error total del ejemplo de regresión hecho con una red neuronal ha mejorado alrededor de \(16\).
Alternativamente, se puede utilizar la poderosa herramienta de R caret
para ajustar mejor su modelo. Cuando se invoca caret
, se puede pasar algunos parámetros de ajuste y técnicas de muestreo para obtener una mejor estimación del error y resultados más precisos, como se muestra aquí:
library(caret)
## Warning: package 'caret' was built under R version 4.0.4
## Loading required package: lattice
## Loading required package: ggplot2
## Warning: package 'ggplot2' was built under R version 4.0.4
mygrid <- expand.grid(.decay = c(0.5, 0.1), .size = c(4, 5, 6))
nnetfit <- train(medv/50 ~ ., data = BostonHousing, method = "nnet",
maxit = 1000, tuneGrid = mygrid, trace = F)
print(nnetfit)
## Neural Network
##
## 506 samples
## 13 predictor
##
## No pre-processing
## Resampling: Bootstrapped (25 reps)
## Summary of sample sizes: 506, 506, 506, 506, 506, 506, ...
## Resampling results across tuning parameters:
##
## decay size RMSE Rsquared MAE
## 0.1 4 0.08072219 0.8053211 0.05674441
## 0.1 5 0.08035113 0.8049357 0.05620143
## 0.1 6 0.07757269 0.8189860 0.05487439
## 0.5 4 0.08897524 0.7722167 0.06226551
## 0.5 5 0.08726191 0.7807634 0.06143389
## 0.5 6 0.08622872 0.7846344 0.06116410
##
## RMSE was used to select the optimal model using the smallest value.
## The final values used for the model were size = 6 and decay = 0.1.
La mejor estimación de error de este caso es de tamaño \(6\), lo que significa \(6\) nodos en una capa oculta de la red, y una caída del parámetro de \(0.1\). La raíz del error cuadrático medio (RMSE) da el mismo error que ha visto anteriormente, pero habiendo tomando la raíz cuadrada. Entonces, para comparar con los resultados vistos anteriormente, el mejor error aquí es como sigue:
0.08168503^2
## [1] 0.006672444
Una mejora notable con respecto a la raíz del error cuadrático medio de \(16\) que vimos anteriormente.
En cierto sentido, se ha demostrado el uso de las redes neuronales para la clasificación a través de las puertas \(AND\) y \(OR\) que se construyeron al principio del capítulo. Estas funciones toman algún tipo de entradas binarias y dan un resultado binario a través de funciones de activación de regresión logística en cada nodo computacional de la red neuronal.
En este caso, se necesita separar los datos en conjuntos de entrenamiento y conjuntos de prueba, que es bastante sencillo. El entrenamiento de la red neuronal con los datos de entrenamiento también tiene sentido a partir de nuestras experiencias pasadas con el enfoque de entrenamiento/prueba para el aprendizaje automático. La diferencia aquí es que cuando se llama la función predict()
, se hace con la opción type=class
. Esto ayuda cuando se trata de datos de alguna clase en lugar de datos numéricos, este último usaría regresión.
iris.df <- iris
smp_size <- floor(0.75 * nrow(iris.df))
set.seed(123)
train_ind <- sample(seq_len(nrow(iris.df)), size = smp_size)
train <- iris.df[train_ind, ]
test <- iris.df[-train_ind, ]
iris.nnet <- nnet(Species ~ ., data = train, size = 4, decay = 0.0001,
maxit = 500, trace = FALSE)
predictions <- predict(iris.nnet, test[, 1:4], type = "class")
Puede ver que la matriz de confusión proporciona un resultado bastante bueno para la clasificación mediante redes neuronales. Piense en el Capítulo 2 y en el ejemplo que usa Kmeans
para la agrupación en clústeres multiclase; no se tienen casos aquí que estén mal etiquetados en comparación con los dos casos mal etiquetados que se vieron anteriormente.
El paquete de Machine Learning en R, caret
, ofrece una agrupación de herramientas muy flexible para utilizar en estos procedimientos de apredizaje automático. En el caso de las redes neuronales, hay más de 15 para elegir, cada una con sus propias ventajas y desventajas. Si nos quedamos con nuestro ejemplo de nnet
por el momento, podemos ejecutar un modelo en forma de intercalación invocando la función train()
y pasándole la opción method='nnet'
. Entonces podemos seguir con nuestros pasos de predicción normales. El poder de la intercalación proviene de la facilidad con la que podemos seleccionar un método diferente con el que comparar los resultados.
En el caso de la regresión, el resultado que se busca será numérico. Entonces, para comparar los resultados entre modelos, debe buscar el RMSE y luego ver cuál tiene el más bajo, lo que indica que este modelo es el más preciso. Para este ejemplo, se usa el conjunto de datos Prestige
del paquete car
. Este conjunto de datos contiene una serie de características relacionadas con las ocupaciones y el prestigio ocupacional percibido con algunas características como la educación, los ingresos y qué porcentaje de los ocupantes en esa profesión son mujeres. Para este ejemplo de regresión, se intentará predecir los ingresos en función del prestigio y la educación:
library(car)
## Warning: package 'car' was built under R version 4.0.4
## Loading required package: carData
library(caret)
trainIndex <- createDataPartition(Prestige$income, p = 0.7, list = F)
prestige.train <- Prestige[trainIndex, ]
prestige.test <- Prestige[-trainIndex, ]
my.grid <- expand.grid(.decay = c(0.5, 0.1), .size = c(5, 6,
7))
prestige.fit <- train(income ~ prestige + education, data = prestige.train,
method = "nnet", maxit = 1000, tuneGrid = my.grid, trace = F,
linout = 1)
prestige.predict <- predict(prestige.fit, newdata = prestige.test)
summary(prestige.test$income)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 611 4504 5962 7196 8077 25879
sqrt(mean((prestige.predict - prestige.test$income)^2))
## [1] 3658.62
De acuerdo con el resultado, el rango de ingresos en el conjunto de datos va desde \(611\) dólares canadienses hasta \(25,879\). El error de \(3,658\) dólares canadienses es alto, pero puede probar con otros tipos de redes neuronales para ver cómo se comparan con el método nnet
:
prestige.fit <- train(income ~ prestige + education, data = prestige.train,
method = "neuralnet")
## Warning in nominalTrainWorkflow(x = x, y = y, wts = weights, info = trainInfo, :
## There were missing values in resampled performance measures.
prestige.predict <- predict(prestige.fit, newdata = prestige.test)
sqrt(mean((prestige.predict - prestige.test$income)^2))
## [1] 5067.804
El resultado de este método es \(5,067.804\). Esa es una mejora con respecto al método nnet
, pero la velocidad a la que se ejecuta este cálculo es más lenta. Aquí es donde debe confiar en el ajuste de sus objetos de entrenamiento para extraer el rendimiento óptimo de cada método diferente que elija.
La clasificación con caret
funciona de manera similar según el método que esté utilizando. Puede utilizar la mayoría de los métodos de intercalación para la clasificación o la regresión, pero algunos son específicos de uno frente a otro. El único método para clasificación explicita de caret
es multinom
, mientras que los métodos neuralnet
, brnn
, qrnn
y mlpSGD
son solo de regresión explícita. Se puede usar el resto para clasificación o regresión:
library("e1071")
## Warning: package 'e1071' was built under R version 4.0.4
iris.caret <- train(Species ~ ., data = train, method = "nnet",
trace = FALSE)
predictions <- predict(iris.caret, test[, 1:4])
table(predictions, test$Species)
##
## predictions setosa versicolor virginica
## setosa 12 0 0
## versicolor 0 16 0
## virginica 0 1 9
El resultado final aquí es el mismo que antes en términos de precisión del modelo, pero la flexibilidad de caret
le permite volver a probar con otros métodos con bastante facilidad:
iris.caret.m <- train(Species ~ ., data = train, method = "multinom",
trace = FALSE)
predictions.m <- predict(iris.caret.m, test[, 1:4])
table(predictions.m, test$Species)
##
## predictions.m setosa versicolor virginica
## setosa 12 0 0
## versicolor 0 16 0
## virginica 0 1 9
¡Es bueno saber que hay otros métodos que también son bastante precisos!