Hola a todos! En la construcción de modelos ML, Python hoy ocupa una posición de liderazgo y es ampliamente popular entre la comunidad de especialistas en Data Science [
1 ].
Como la mayoría de los desarrolladores, Python nos atrae con su simplicidad y sintaxis concisa. Lo usamos para resolver problemas de aprendizaje automático utilizando redes neuronales artificiales. Sin embargo, en la práctica, el lenguaje de desarrollo de productos no siempre es Python, y esto nos obliga a resolver problemas de integración adicionales.
En este artículo hablaré sobre las soluciones a las que llegamos cuando necesitábamos asociar el modelo Keras de Python con Java.
A qué prestamos atención:
- Incluye paquetes de modelos Keras y Java;
- Prepararse para trabajar con el marco DeepLearning4j (DL4J para abreviar);
- Importar un modelo de Keras en DL4J (cuidadosamente, la sección contiene múltiples ideas): cómo registrar capas, qué limitaciones tiene el módulo de importación, cómo verificar los resultados de su trabajo.
¿Por qué leer?
- Para ahorrar tiempo al principio, si enfrentará la tarea de una integración similar;
- Para saber si nuestra solución es adecuada para usted y si puede reutilizar nuestra experiencia.
Característica integral sobre la importancia de los marcos de aprendizaje profundo [
2 ].
Puede encontrar un resumen de los marcos de aprendizaje profundo más populares aquí [
3 ] y aquí [
4 ].
Como puede ver, la mayoría de estos marcos se basan en Python y C ++: usan C ++ como el núcleo para acelerar las operaciones básicas y altamente cargadas, y Python como la interfaz de interacción para acelerar el desarrollo.
De hecho, muchos lenguajes de desarrollo son mucho más extensos. Java es el líder en desarrollo de productos para grandes empresas y organizaciones. Algunos marcos populares para redes neuronales tienen puertos para Java en forma de carpetas JNI / JNA, pero en este caso existe la necesidad de construir un proyecto para cada arquitectura y la ventaja de Java en el tema del desenfoque multiplataforma. Este matiz puede ser extremadamente importante en soluciones replicadas.
Otro enfoque alternativo es usar Jython para compilar en Java bytecode; pero hay un inconveniente aquí: soporte solo para la segunda versión de Python, así como la capacidad limitada de usar bibliotecas de Python de terceros.
Para simplificar el desarrollo de soluciones de redes neuronales en Java, se está desarrollando el marco DeepLearning4j (DL4J para abreviar). DL4 además de la API de Java ofrece un conjunto de modelos previamente entrenados [
5 ]. En general, esta herramienta de desarrollo es difícil de competir con TensorFlow. TensorFlow supera a DL4J con documentación más detallada y una serie de ejemplos, capacidades técnicas, tamaños de comunidad y desarrollo acelerado. Sin embargo, la tendencia a la que se adhiere Skymind es bastante prometedora. Los competidores significativos en Java para esta herramienta aún no son visibles.
La biblioteca DL4J es una de las pocas (si no la única) que permite importar modelos Keras, y amplía su funcionalidad con las capas familiares de Keras [
6 ]. La biblioteca DL4J contiene un directorio con ejemplos de la implementación de modelos ML de redes neuronales (ejemplo dl4j). En nuestro caso, las sutilezas de implementar estos modelos en Java no son tan interesantes. Se prestará más atención a la importación del modelo entrenado Keras / TF a Java utilizando métodos DL4J.
Empezando
Antes de comenzar, debe instalar los programas necesarios:
- Java versión 1.7 (versión de 64 bits) y superior.
- Sistema de construcción del proyecto Apache Maven.
- IDE para elegir: Intellij IDEA, Eclipse, Netbeans. Los desarrolladores recomiendan la primera opción y, además, se analizan los ejemplos de capacitación disponibles.
- Git (para clonar un proyecto en tu PC).
Puede encontrar una descripción detallada con un ejemplo de lanzamiento aquí [
7 ] o en el video [
8 ].
Para importar el modelo, los desarrolladores de DL4J sugieren usar el
módulo de importación
KerasModelImport (apareció en octubre de 2016). El funcional del módulo es compatible con ambas arquitecturas de modelos de Keras: es secuencial (analógico en java clase MultiLayerNetwork) y funcional (analógico en java clase ComputationGraph). El modelo se importa como un todo en formato HDF5, o 2 archivos separados: el peso del modelo con la extensión h5 y el archivo json que contiene la arquitectura de red neuronal.
Para un inicio rápido, los desarrolladores de DL4J prepararon un análisis paso a paso de un ejemplo simple en el conjunto de datos de iris de Fisher para un modelo de tipo secuencial [
9 ]. Se consideró otro ejemplo de capacitación desde la perspectiva de importar modelos de dos maneras (1: en formato HDF5 completo; 2: en archivos separados: pesos de modelo (extensión h5) y arquitectura (extensión json)), seguido de una comparación de los resultados de los modelos Python y Java [
10 ]. Esto concluye la discusión de las capacidades prácticas del módulo de importación.
También hay TF en Java, pero está en un estado experimental y los desarrolladores no dan ninguna garantía de su funcionamiento estable [
11 ]. Hay problemas con el control de versiones, y TF en Java tiene una API incompleta, por lo que esta opción no se considerará aquí.
Características del modelo original Keras / TF:
Importar una red neuronal es sencillo. Con más detalle en el código analizaremos un ejemplo de integración de una red neuronal con una arquitectura más complicada.
No debe entrar en los aspectos prácticos de este modelo, es indicativo desde el punto de vista de la contabilidad de las capas (en particular, el registro de capas Lambda), algunas sutilezas y limitaciones del módulo de importación, así como DL4J en su conjunto. En la práctica, los matices observados pueden requerir ajustes en la arquitectura de la red o abandonar por completo el enfoque de lanzar el modelo a través de DL4J.
Características del modelo:
1. Tipo de modelo: funcional (red con ramificación);
2. Los parámetros de entrenamiento (el tamaño del lote, el número de eras) se seleccionan pequeños: el tamaño del lote - 100, el número de eras - 10, los pasos por era - 10;
3. 13 capas, un resumen de las capas se muestra en la figura:

Descripción de capa corta- input_1: capa de entrada, acepta un tensor bidimensional (representado por una matriz);
- lambda_1: la capa de usuario, en nuestro caso, hace que el relleno en TF del tensor tenga los mismos valores numéricos;
- incrustación_1: crea la incrustación (representación vectorial) para la secuencia de entrada de datos de texto (convierte el tensor 2-D en 3-D);
- conv1d_1: capa convolucional 1-D;
- lstm_2: capa LSTM (va después de incrustar la capa_1 (No. 3));
- lstm_1 - capa LSTM (va después de la capa conv1d (No. 4));
- lambda_2 es la capa de usuario donde el tensor se trunca después de la capa lstm_2 (No. 5) (la operación opuesta al relleno en la capa lambda_1 (No. 2));
- lambda_3 es la capa de usuario donde el tensor se trunca después de las capas lstm_1 (No. 6) y conv1d_1 (No. 4) (la operación opuesta al relleno en la capa lambda_1 (No. 2));
- concatenate_1: unión de capas truncadas (No. 7) y (No. 8);
- dense_1: una capa totalmente conectada de 8 neuronas y una función de activación lineal exponencial "elu";
- batch_normalization_1: capa de normalización;
- dense_2 - capa completamente conectada de 1 neurona y función de activación sigmoidea "sigmoidea";
- lambda_4: una capa de usuario donde se realiza la compresión de la capa anterior (compresión en TF).
4. Función de pérdida - binary_crossentropy
5. Métrica de calidad del modelo - media armónica (medida F)
En nuestro caso, el tema de las métricas de calidad no es tan importante como la exactitud de la importación. La exactitud de la importación está determinada por la coincidencia de los resultados en los modelos NN de Python y Java que funcionan en el modo de inferencia.
Importar modelos Keras en DL4J:
Versiones utilizadas: Tensorflow 1.5.0 y Keras 2.2.5. En nuestro caso, el modelo de Python fue cargado en su conjunto por el archivo HDF5.
Al importar un modelo en DL4J, el módulo de importación no proporciona métodos API para pasar parámetros adicionales: el nombre del módulo de tensorflow (desde donde se importaron las funciones al construir el modelo).
En términos generales, DL4J solo funciona con las funciones de Keras, se proporciona una lista exhaustiva en la sección de importación de Keras [
6 ], por lo que si se creó un modelo en Keras utilizando métodos de TF (como en nuestro caso), el módulo de importación no podrá identificarlos.
Pautas generales para importar un modelo
Obviamente, trabajar con el modelo Keras implica su entrenamiento repetido. Con este fin, para ahorrar tiempo, se establecieron los parámetros de entrenamiento (1 época) y 1 paso por época (pasos_por_epoca).
Cuando importa por primera vez un modelo, en particular con capas personalizadas únicas y combinaciones de capas raras, el éxito es poco probable. Por lo tanto, se recomienda llevar a cabo el proceso de importación de forma iterativa: reduzca el número de capas del modelo Keras hasta que pueda importar y ejecutar el modelo en Java sin errores. Luego, agregue una capa a la vez al modelo Keras e importe el modelo resultante a Java, resolviendo los errores que ocurren.
Uso de la función de pérdida de TF
Para demostrar que, al importar a Java, la función de pérdida del modelo entrenado debe ser de Keras, utilizamos log_loss de tensorflow (como la más similar a la función custom_loss). Recibimos el siguiente error en la consola:
Exception in thread "main" org.deeplearning4j.nn.modelimport.keras.exceptions.UnsupportedKerasConfigurationException: Unknown Keras loss function log_loss.
Sustitución de métodos TF con Keras
En nuestro caso, las funciones del módulo TF se usan 2 veces y en todos los casos se encuentran solo en capas lambda.
Las capas Lambda son capas personalizadas que se utilizan para agregar una función arbitraria.
Nuestro modelo tiene solo 4 capas lambda. El hecho es que en Java es necesario registrar estas capas lambda manualmente a través de KerasLayer.registerLambdaLayer (de lo contrario, obtendremos un error [
12 ]). En este caso, la función definida dentro de la capa lambda debería ser una función de las bibliotecas Java correspondientes. En Java no hay ejemplos de registro de estas capas, así como documentación exhaustiva para esto; Un ejemplo está aquí [
13 ]. Consideraciones generales fueron tomadas de los ejemplos [
14 ,
15 ].
Considere secuencialmente registrar todas las capas lambda del modelo en Java:
1) Capa Lambda para agregar constantes al tensor (matriz) un número finito de veces a lo largo de las direcciones dadas (en nuestro caso, izquierda y derecha):
La entrada de esta capa está conectada a la entrada del modelo.
1.1) Capa de Python:
padding = keras.layers.Lambda(lambda x: tf.pad(x, paddings=[[0, 0], [10, 10]], constant_values=1))(embedding)
Para mayor claridad, las funciones de esta capa funcionan, sustituimos explícitamente los valores numéricos en las capas de Python.
Tabla con un ejemplo de un tensor arbitrario 2x2 1.2) Capa de Java:
KerasLayer.registerLambdaLayer("lambda_1", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.nn().pad(sdVariable, new int[][]{ { 0, 0 }, { 10, 10 }}, 1); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.feedForward(20); } });
En todas las capas lambda registradas en Java, se redefinen 2 funciones:
La primera función "definelayer" es responsable del método utilizado (en absoluto un hecho obvio: este método solo se puede utilizar desde el backend nn ()); getOutputType es responsable de la salida de la capa registrada, el argumento es un parámetro numérico (aquí 20, pero generalmente se permite cualquier valor entero). Parece inconsistente, pero funciona así.
2) Capa Lambda para recortar el tensor (matriz) a lo largo de las direcciones dadas (en nuestro caso, izquierda y derecha):
En este caso, la capa LSTM ingresa la entrada de la capa lambda.
2.1) Capa de Python:
slicing_lstm = keras.layers.Lambda(lambda x: x[:, 10:-10])(lstm)
Tabla con un ejemplo de un tensor arbitrario 2x22x5 2.2) Capa de Java:
KerasLayer.registerLambdaLayer("lambda_2", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.stridedSlice(sdVariable, new int[]{ 0, 0, 10 }, new int[]{ (int)sdVariable.getShape()[0], (int)sdVariable.getShape()[1], (int)sdVariable.getShape()[2]-10}, new int[]{ 1, 1, 1 }); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.recurrent(60); } });
En el caso de esta capa, el parámetro InputType cambió de feedforward (20) a recurrente (60). En el argumento recurrente, el número puede ser cualquier número entero (distinto de cero), pero su suma con el argumento recurrente de la siguiente capa lambda debe dar 160 (es decir, en la siguiente capa, el argumento debe ser 100). El número 160 se debe al hecho de que el tensor con la dimensión (Ninguno, Ninguno, 160) debe recibirse en la entrada concatenada_1 de la capa.
Los primeros 2 argumentos son variables, dependiendo del tamaño de la cadena de entrada.
3) Capa Lambda para recortar el tensor (matriz) a lo largo de las direcciones dadas (en nuestro caso, izquierda y derecha):
La entrada de esta capa es la capa LSTM, frente a la cual está la capa conv1_d
3.1) Capa de Python:
slicing_convolution = keras.layers.Lambda(lambda x: x[:,10:-10])(lstm_conv)
Esta operación es completamente idéntica a la operación en el párrafo 2.1.
3.2) capa de Java:
KerasLayer.registerLambdaLayer("lambda_3", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.stridedSlice(sdVariable, new int[]{ 0, 0, 10 }, new int[]{ (int)sdVariable.getShape()[0], (int)sdVariable.getShape()[1], (int)sdVariable.getShape()[2]-10}, new int[]{ 1, 1, 1 }); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.recurrent(100); } });
Esta capa lambda repite la capa lambda anterior con la excepción del parámetro recurrente (100). Por qué se toma "100" se observa en la descripción de la capa anterior.
En los puntos 2 y 3, las capas lambda se ubican después de las capas LSTM, por lo que se utiliza el tipo recurrente. Pero si antes de la capa lambda no había LSTM, sino conv1d_1, entonces todavía es necesario establecer recurrente (parece inconsistente, pero funciona así).
4) Capa Lambda para comprimir la capa anterior:
La entrada de esta capa es una capa totalmente conectada.
4.1) Capa de Python:
squeeze = keras.layers.Lambda(lambda x: tf.squeeze( x, axis=-1))(dense)
Tabla con un ejemplo de un tensor arbitrario 2x4x1 4.2) Capa de Java:
KerasLayer.registerLambdaLayer("lambda_4", new SameDiffLambdaLayer() { @Override public SDVariable defineLayer(SameDiff sameDiff, SDVariable sdVariable) { return sameDiff.squeeze(sdVariable, -1); } @Override public InputType getOutputType(int layerIndex, InputType inputType) { return InputType.feedForward(15); } });
La entrada de esta capa recibe una capa completamente conectada, InputType para esta capa feedForward (15), el parámetro 15 no afecta el modelo (se permite cualquier valor entero).
Descargar modelo importado
El modelo se carga a través del módulo ComputationGraph:
ComputationGraph model = org.deeplearning4j.nn.modelimport.keras.KerasModelImport.importKerasModelAndWeights("/home/user/Models/model1_functional.h5");
Salida de datos a la consola Java
En Java, en particular en DL4J, los tensores se escriben como matrices de la biblioteca Nd4j de alto rendimiento, que puede considerarse un análogo de la biblioteca Numpy en Python.
Digamos que nuestra cadena de entrada consta de 4 caracteres. Los símbolos se representan como enteros (como índices), por ejemplo, de acuerdo con alguna numeración. Se crea una matriz de la dimensión correspondiente (4) para ellos.
Por ejemplo, tenemos 4 caracteres codificados por índice: 1, 3, 4, 8.
Código en Java:
INDArray myArray = Nd4j.zeros(1,4);
La consola mostrará las probabilidades para cada elemento de entrada.
Modelos importados
La arquitectura de la red neuronal original y los pesos se importan sin errores. Tanto los modelos de red neuronal Keras como Java en modo de inferencia están de acuerdo con los resultados.
Modelo de Python:
Modelo Java:
En realidad, importar modelos no es tan simple. A continuación destacaremos brevemente algunos puntos que en algunos casos pueden ser críticos.
1) La capa de normalización del parche no funciona después de las capas recursivas. El problema ha estado abierto en GitHub durante casi un año [
16 ]. Por ejemplo, si agrega esta capa al modelo (después de la capa de contacto), obtenemos el siguiente error:
Exception in thread "main" java.lang.IllegalStateException: Invalid input type: Batch norm layer expected input of type CNN, CNN Flat or FF, got InputTypeRecurrent(160) for layer index -1, layer name = batch_normalization_1
En la práctica, el modelo se negó a funcionar, citando un error similar cuando se agregó la capa de normalización después de conve1d. Después de una capa totalmente conectada, la adición funciona a la perfección.
2) Después de una capa completamente conectada, al configurar la capa Flatten se produce un error. Un error similar se menciona en Stackoverflow [
17 ]. Durante seis meses, no hay comentarios.
Definitivamente, estas no son todas las restricciones que puede encontrar al trabajar con DL4J.
El tiempo de funcionamiento final del modelo está aquí [
18 ].
Conclusión
En conclusión, se puede observar que los modelos Keras entrenados importados sin dolor en DL4J solo pueden ser para casos simples (por supuesto, si no tiene esa experiencia y, de hecho, un buen dominio de Java).
Cuantas menos capas de usuario, más fácil será importar el modelo, pero si la arquitectura de la red es compleja, tendrá que pasar mucho tiempo transfiriéndola a DL4J.
El soporte documental del módulo de importación desarrollado, el número de ejemplos relacionados, parecía bastante húmedo. En cada etapa, surgen nuevas preguntas: cómo registrar las capas de Lambda, el significado de los parámetros, etc.
Dada la velocidad de la complejidad de las arquitecturas de redes neuronales y la interacción entre capas, la complejidad de las capas, DL4J aún no se ha desarrollado activamente para alcanzar el nivel de los marcos de gama alta para trabajar con redes neuronales artificiales.
En cualquier caso, los muchachos son dignos de respeto por su trabajo y me gustaría ver que el desarrollo de esta área continúe.
Referencias- Los 5 mejores lenguajes de programación para el campo de la inteligencia artificial
- Deep Learning Framework Power Scores 2018
- Comparación de software de aprendizaje profundo
- Los 9 marcos principales en el mundo de la inteligencia artificial
- Aprendizaje profundo 4j. Modelos disponibles
- Aprendizaje profundo 4j. Importación del modelo Keras. Funciones compatibles
- Deeplearning4j. Inicio rápido
- Lección 0: Comenzando con DeepLearning4j
- Deeplearing4j: importación del modelo Keras
- Conferencia 7 | Importar modelo Keras
- Instalar TensorFlow para Java
- Usando capas de Keras
- DeepLearning4j: Clase KerasLayer
- DeepLearning4j: SameDiffLambdaLayer.java
- DeepLearning4j: KerasLambdaTest.java
- DeepLearning4j: BatchNorm con RecurrentInputType
- StackOverFlow: Problema al abrir un modelo de Keras en Java con deeplearning4j (https://deeplearning4j.org/)
- GitHub: código completo para el modelo en cuestión
- Skymind: Comparación de marcos de AI