1. Introducción
En este artículo, crearemos una red neuronal simple con la biblioteca deeplearning4j (dl4j), una herramienta moderna y poderosa para el aprendizaje automático.
Antes de comenzar, no es que esta guía no requiera un conocimiento profundo de álgebra lineal, estadística, teoría del aprendizaje automático y muchos otros temas necesarios para un ingeniero de ML bien fundamentado.
2. ¿Qué es el aprendizaje profundo?
Las redes neuronales son modelos computacionales que consisten en capas de nodos interconectados.
Los nodos son procesadores de datos numéricos similares a neuronas. Toman datos de sus entradas, aplican algunos pesos y funciones a estos datos y envían los resultados a las salidas. Dicha red se puede entrenar con algunos ejemplos de los datos de origen.
Básicamente, el entrenamiento consiste en guardar algún estado numérico (pesos) en los nodos, lo que luego afecta el cálculo. Los ejemplos de capacitación pueden contener elementos de datos con características y ciertas clases conocidas de estos elementos (por ejemplo, "este conjunto de 16 × 16 píxeles contiene una letra" a "escrita a mano).
Una vez finalizado el entrenamiento, una red neuronal puede derivar información de nuevos datos, incluso si no ha visto estos elementos de datos en particular antes . Una red bien modelada y entrenada puede reconocer imágenes, cartas escritas a mano, discursos, procesar datos estadísticos para producir resultados para inteligencia empresarial y mucho más.
Las redes neuronales profundas se hicieron posibles en los últimos años, con el avance de la computación paralela y de alto rendimiento. Estas redes se diferencian de las redes neuronales simples en que consisten en múltiples capas intermedias (u ocultas) . Esta estructura permite a las redes procesar datos de una manera mucho más complicada (de forma recursiva, recurrente, convolucional, etc.) y extraer mucha más información de ella.
3. Configuración del proyecto
Para usar la biblioteca, necesitamos al menos Java 7. Además, debido a algunos componentes nativos, solo funciona con la versión JVM de 64 bits.
Antes de comenzar con la guía, verifiquemos si se cumplen los requisitos:
$ java -version java version "1.8.0_131" Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
Primero, agreguemos las bibliotecas requeridas a nuestro archivo pom.xml de Maven . Extraeremos la versión de la biblioteca a una entrada de propiedad (para obtener la última versión de las bibliotecas, consulte el repositorio de Maven Central):
0.9.1 org.nd4j nd4j-native-platform ${dl4j.version} org.deeplearning4j deeplearning4j-core ${dl4j.version}
Tenga en cuenta que la dependencia de nd4j-native-platform es una de las varias implementaciones disponibles.
Se basa en bibliotecas nativas disponibles para muchas plataformas diferentes (macOS, Windows, Linux, Android, etc.). También podríamos cambiar el backend a nd4j-cuda-8.0-platform , si quisiéramos ejecutar cálculos en una tarjeta gráfica que admita el modelo de programación CUDA.
4. Preparación de los datos
4.1. Preparar el archivo DataSet
Escribiremos el "Hola mundo" del aprendizaje automático: clasificación del conjunto de datos de la flor del iris. Este es un conjunto de datos que se recopilaron de las flores de diferentes especies ( Iris setosa , Iris versicolor e Iris virginica ).
Estas especies difieren en la longitud y el ancho de los pétalos y sépalos. Sería difícil escribir un algoritmo preciso que clasifique un elemento de datos de entrada (es decir, determine a qué especie pertenece una flor en particular). Pero una red neuronal bien entrenada puede clasificarlo rápidamente y con pequeños errores.
Vamos a usar una versión CSV de estos datos, donde las columnas 0..3 contienen las diferentes características de la especie y la columna 4 contiene la clase del registro, o la especie, codificada con un valor 0, 1 o 2:
5.1,3.5,1.4,0.2,0 4.9,3.0,1.4,0.2,0 4.7,3.2,1.3,0.2,0 … 7.0,3.2,4.7,1.4,1 6.4,3.2,4.5,1.5,1 6.9,3.1,4.9,1.5,1 …
4.2. Vectorizar y leer los datos
Codificamos la clase con un número porque las redes neuronales funcionan con números. La transformación de elementos de datos del mundo real en series de números (vectores) se llama vectorización ; deeplearning4j usa la biblioteca datavec para hacer esto.
Primero, usemos esta biblioteca para ingresar el archivo con los datos vectorizados. Al crear CSVRecordReader , podemos especificar el número de líneas que se deben omitir (por ejemplo, si el archivo tiene una línea de encabezado) y el símbolo de separación (en nuestro caso, una coma):
try (RecordReader recordReader = new CSVRecordReader(0, ',')) { recordReader.initialize(new FileSplit( new ClassPathResource("iris.txt").getFile())); // … }
Para iterar sobre los registros, podemos usar cualquiera de las múltiples implementaciones de la interfaz DataSetIterator . Los conjuntos de datos pueden ser bastante masivos y la capacidad de paginar o almacenar en caché los valores podría resultar útil.
Pero nuestro pequeño conjunto de datos contiene solo 150 registros, así que leamos todos los datos en la memoria a la vez con una llamada de iterator.next () .
También especificamos el índice de la columna de la clase que en nuestro caso es lo mismo que el recuento de características (4) y el número total de clases (3).
Además, tenga en cuenta que necesitamos mezclar el conjunto de datos para deshacernos del orden de clases en el archivo original.
Especificamos una semilla aleatoria constante (42) en lugar de la llamada predeterminada System.currentTimeMillis () para que los resultados de la mezcla sean siempre los mismos. Esto nos permite obtener resultados estables cada vez que ejecutamos el programa:
DataSetIterator iterator = new RecordReaderDataSetIterator( recordReader, 150, FEATURES_COUNT, CLASSES_COUNT); DataSet allData = iterator.next(); allData.shuffle(42);
4.3. Normalización y división
Otra cosa que debemos hacer con los datos antes del entrenamiento es normalizarlos. La normalización es un proceso de dos fases:
- recopilación de algunas estadísticas sobre los datos (ajuste)
- cambiar (transformar) los datos de alguna manera para que sean uniformes
La normalización puede diferir para diferentes tipos de datos.
Por ejemplo, si queremos procesar imágenes de varios tamaños, primero debemos recopilar las estadísticas de tamaño y luego escalar las imágenes a un tamaño uniforme.
Pero para los números, la normalización generalmente significa transformarlos en una denominada distribución normal. La clase NormalizerStandardize puede ayudarnos con eso:
DataNormalization normalizer = new NormalizerStandardize(); normalizer.fit(allData); normalizer.transform(allData);
Ahora que los datos están preparados, necesitamos dividir el conjunto en dos partes.
La primera parte se utilizará en una sesión de formación. Usaremos la segunda parte de los datos (que la red no vería en absoluto) para probar la red entrenada.
This would allow us to verify that the classification works correctly. We will take 65% of the data (0.65) for the training and leave the rest 35% for the testing:
SplitTestAndTrain testAndTrain = allData.splitTestAndTrain(0.65); DataSet trainingData = testAndTrain.getTrain(); DataSet testData = testAndTrain.getTest();
5. Preparing the Network Configuration
5.1. Fluent Configuration Builder
Now we can build a configuration of our network with a fancy fluent builder:
MultiLayerConfiguration configuration = new NeuralNetConfiguration.Builder() .iterations(1000) .activation(Activation.TANH) .weightInit(WeightInit.XAVIER) .learningRate(0.1) .regularization(true).l2(0.0001) .list() .layer(0, new DenseLayer.Builder().nIn(FEATURES_COUNT).nOut(3).build()) .layer(1, new DenseLayer.Builder().nIn(3).nOut(3).build()) .layer(2, new OutputLayer.Builder( LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) .activation(Activation.SOFTMAX) .nIn(3).nOut(CLASSES_COUNT).build()) .backprop(true).pretrain(false) .build();
Even with this simplified fluent way of building a network model, there’s a lot to digest and a lot of parameters to tweak. Let’s break this model down.
5.2. Setting Network Parameters
The iterations() builder method specifies the number of optimization iterations.
The iterative optimization means performing multiple passes on the training set until the network converges to a good result.
Usually, when training on real and large datasets, we use multiple epochs (complete passes of data through the network) and one iteration for each epoch. But since our initial dataset is minimal, we'll use one epoch and multiple iterations.
The activation() is a function that runs inside a node to determine its output.
The simplest activation function would be linear f(x) = x. But it turns out that only non-linear functions allow networks to solve complex tasks by using a few nodes.
There are lots of different activation functions available which we can look up in the org.nd4j.linalg.activations.Activation enum. We could also write our activation function if needed. But we'll use the provided hyperbolic tangent (tanh) function.
The weightInit() method specifies one of the many ways to set up the initial weights for the network. Correct initial weights can profoundly affect the results of the training. Without going too much into the math, let’s set it to a form of Gaussian distribution (WeightInit.XAVIER), as this is usually a good choice for a start.
All other weight initialization methods can be looked up in the org.deeplearning4j.nn.weights.WeightInit enum.
Learning rate is a crucial parameter that profoundly affects the ability of the network to learn.
We could spend a lot of time tweaking this parameter in a more complex case. But for our simple task, we'll use a pretty significant value of 0.1 and set it up with the learningRate() builder method.
One of the problems with training neural networks is a case of overfitting when a network “memorizes” the training data.
This happens when the network sets excessively high weights for the training data and produces bad results on any other data.
To solve this issue, we’re going to set up l2 regularization with the line .regularization(true).l2(0.0001). Regularization “penalizes” the network for too large weights and prevents overfitting.
5.3. Building Network Layers
Next, we create a network of dense (also known as fully connect) layers.
The first layer should contain the same amount of nodes as the columns in the training data (4).
The second dense layer will contain three nodes. This is the value we can variate, but the number of outputs in the previous layer has to be the same.
The final output layer should contain the number of nodes matching the number of classes (3). The structure of the network is shown in the picture:

After successful training, we'll have a network that receives four values via its inputs and sends a signal to one of its three outputs. This is a simple classifier.
Finally, to finish building the network, we set up back propagation (one of the most effective training methods) and disable pre-training with the line .backprop(true).pretrain(false).
6. Creating and Training a Network
Ahora vamos a crear una red neuronal a partir de la configuración, inicializarla y ejecutarla:
MultiLayerNetwork model = new MultiLayerNetwork(configuration); model.init(); model.fit(trainingData);
Ahora podemos probar el modelo entrenado usando el resto del conjunto de datos y verificar los resultados con métricas de evaluación para tres clases:
INDArray output = model.output(testData.getFeatureMatrix()); Evaluation eval = new Evaluation(3); eval.eval(testData.getLabels(), output);
Si ahora imprimimos eval.stats () , veremos que nuestra red es bastante buena para clasificar flores de iris, aunque confundió la clase 1 con la clase 2 tres veces.
Examples labeled as 0 classified by model as 0: 19 times Examples labeled as 1 classified by model as 1: 16 times Examples labeled as 1 classified by model as 2: 3 times Examples labeled as 2 classified by model as 2: 15 times ==========================Scores======================================== # of classes: 3 Accuracy: 0.9434 Precision: 0.9444 Recall: 0.9474 F1 Score: 0.9411 Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes) ========================================================================
El generador de configuración fluido nos permite agregar o modificar capas de la red rápidamente, o ajustar algunos otros parámetros para ver si nuestro modelo se puede mejorar.
7. Conclusión
En este artículo, hemos construido una red neuronal simple pero poderosa utilizando la biblioteca deeplearning4j.
Como siempre, el código fuente del artículo está disponible en GitHub.