Si vous n'avez pas Python, mais qu'il existe un modèle Keras et Java

Bonjour à tous! Dans la construction de modèles ML, Python occupe aujourd'hui une position de leader et est largement populaire parmi la communauté des spécialistes de la science des données [ 1 ].

Comme la plupart des développeurs, Python nous attire par sa simplicité et sa syntaxe concise. Nous l'utilisons pour résoudre des problèmes d'apprentissage automatique en utilisant des réseaux de neurones artificiels. Cependant, dans la pratique, le langage de développement de produit n'est pas toujours Python, ce qui nous oblige à résoudre des problèmes d'intégration supplémentaires.

Dans cet article, je parlerai des solutions auxquelles nous sommes parvenus lorsque nous devions associer le modèle Keras de Python à Java.

À quoi nous prêtons attention:

  • Comprend les bundles Keras model et Java;
  • Se préparer à travailler avec le framework DeepLearning4j (DL4J pour faire court);
  • Importer un modèle Keras dans DL4J (soigneusement, la section contient plusieurs informations) - comment enregistrer des couches, quelles sont les limites du module d'importation, comment vérifier les résultats de votre travail.

Pourquoi lire?

  • Pour gagner du temps au début, si vous serez confronté à la tâche d'une intégration similaire;
  • Pour savoir si notre solution vous convient et si vous pouvez réutiliser notre expérience.

image alt

Caractéristique intégrale sur l'importance des cadres d'apprentissage profond [ 2 ].

Un résumé des cadres d'apprentissage en profondeur les plus populaires peut être trouvé ici [ 3 ] et ici [ 4 ].

Comme vous pouvez le voir, la plupart de ces frameworks sont basés sur Python et C ++: ils utilisent C ++ comme noyau pour accélérer les opérations de base et très chargées, et Python comme interface d'interaction pour accélérer le développement.

En fait, de nombreux langages de développement sont beaucoup plus étendus. Java est le leader du développement de produits pour les grandes entreprises et organisations. Certains frameworks populaires pour les réseaux de neurones ont des ports pour Java sous la forme de liants JNI / JNA, mais dans ce cas, il est nécessaire de construire un projet pour chaque architecture et l'avantage de Java dans le problème du flou multiplateforme. Cette nuance peut être extrêmement importante dans les solutions répliquées.

Une autre approche alternative consiste à utiliser Jython pour compiler en Java bytecode; mais il y a un inconvénient ici - la prise en charge uniquement de la 2e version de Python, ainsi que la capacité limitée à utiliser des bibliothèques Python tierces.

Pour simplifier le développement de solutions de réseaux de neurones en Java, le framework DeepLearning4j (DL4J pour faire court) est en cours de développement. DL4 en plus de l'API Java propose un ensemble de modèles pré-formés [ 5 ]. En général, cet outil de développement est difficile à concurrencer TensorFlow. TensorFlow surpasse DL4J avec une documentation plus détaillée et un certain nombre d'exemples, des capacités techniques, des tailles de communauté et un développement rapide. Néanmoins, la tendance à laquelle Skymind adhère est assez prometteuse. Les concurrents importants en Java pour cet outil ne sont pas encore visibles.

La bibliothèque DL4J est l'une des rares (sinon la seule) qui permet d'importer des modèles Keras, elle se développe en fonctionnalités avec les couches familières à Keras [ 6 ]. La bibliothèque DL4J contient un répertoire avec des exemples de mise en œuvre de modèles ML de réseau neuronal (exemple dl4j). Dans notre cas, les subtilités de l'implémentation de ces modèles en Java ne sont pas si intéressantes. Une attention plus détaillée sera accordée à l'importation du modèle Keras / TF formé en Java à l'aide des méthodes DL4J.

Pour commencer


Avant de commencer, vous devez installer les programmes nécessaires:

  1. Java version 1.7 (version 64 bits) et supérieure.
  2. Système de construction de projet Apache Maven.
  3. IDE au choix: Intellij IDEA, Eclipse, Netbeans. Les développeurs recommandent la première option, et en plus, les exemples de formation disponibles y sont discutés.
  4. Git (pour cloner un projet sur votre PC).

Une description détaillée avec un exemple de lancement peut être trouvée ici [ 7 ] ou dans la vidéo [ 8 ].

Pour importer le modèle, les développeurs DL4J suggèrent d'utiliser le module d' importation KerasModelImport (apparu en octobre 2016). La fonctionnalité du module prend en charge les deux architectures de modèles de Keras - il est séquentiel (analogique en classe Java MultiLayerNetwork) et fonctionnel (analogique en classe Java ComputationGraph). Le modèle est importé dans son ensemble au format HDF5 ou 2 fichiers distincts - le poids du modèle avec l'extension h5 et le fichier json contenant l'architecture du réseau de neurones.

Pour un démarrage rapide, les développeurs DL4J ont préparé une analyse pas à pas d'un exemple simple sur le jeu de données Fisher iris pour un modèle de type Sequential [ 9 ]. Un autre exemple de formation a été considéré du point de vue de l'importation de modèles de deux manières (1: au format HDF5 complet; 2: dans des fichiers séparés - poids du modèle (extension h5) et architecture (extension json)), suivi d'une comparaison des résultats des modèles Python et Java [ 10 ]. Ceci conclut la discussion sur les capacités pratiques du module d'importation.

Il existe également TF en Java, mais il est à l'état expérimental et les développeurs ne donnent aucune garantie de son fonctionnement stable [ 11 ]. Il y a des problèmes avec la gestion des versions et TF en Java a une API incomplète - c'est pourquoi cette option ne sera pas considérée ici.

Caractéristiques du modèle Keras / TF d'origine:


L'importation d'un réseau de neurones est simple. Plus en détail dans le code nous analyserons un exemple d'intégration d'un réseau neuronal avec une architecture plus compliquée.

Vous ne devriez pas entrer dans les aspects pratiques de ce modèle, il est indicatif du point de vue de la prise en compte des couches (en particulier, l'enregistrement des couches Lambda), de certaines subtilités et limitations du module d'importation, ainsi que de DL4J dans son ensemble. Dans la pratique, les nuances notées peuvent nécessiter des ajustements à l'architecture du réseau, ou abandonner complètement l'approche de lancement du modèle via DL4J.

Caractéristiques du modèle:

1. Type de modèle - Fonctionnel (réseau avec branchement);

2. Les paramètres d'apprentissage (la taille du lot, le nombre d'époques) sont sélectionnés petit: la taille du lot - 100, le nombre d'époques - 10, les étapes par époque - 10;

3. 13 couches, un résumé des couches est montré dans la figure:

image alt

Description courte de la couche
  1. input_1 - couche d'entrée, accepte un tenseur bidimensionnel (représenté par une matrice);
  2. lambda_1 - la couche utilisateur, dans notre cas, fait le remplissage en TF du tenseur les mêmes valeurs numériques;
  3. embedding_1 - construit l'incorporation (représentation vectorielle) pour la séquence d'entrée des données de texte (convertit le tenseur 2D en 3D);
  4. conv1d_1 - Couche convolutionnelle 1-D;
  5. lstm_2 - Couche LSTM (après la couche embedding_1 (n ° 3));
  6. lstm_1 - couche LSTM (va après la couche conv1d (n ° 4));
  7. lambda_2 est la couche utilisateur où le tenseur est tronqué après la couche lstm_2 (n ° 5) (l'opération opposée au remplissage dans la couche lambda_1 (n ° 2));
  8. lambda_3 est la couche utilisateur où le tenseur est tronqué après les couches lstm_1 (n ° 6) et conv1d_1 (n ° 4) (l'opération opposée au remplissage dans la couche lambda_1 (n ° 2));
  9. concaténat_1 - collage des couches tronquées (n ° 7) et (n ° 8);
  10. dense_1 - une couche entièrement connectée de 8 neurones et une fonction d'activation linéaire exponentielle "elu";
  11. batch_normalization_1 - couche de normalisation;
  12. dense_2 - couche entièrement connectée de 1 neurone et fonction d'activation sigmoïde "sigmoïde";
  13. lambda_4 - une couche utilisateur où la compression de la couche précédente (compression dans TF) est effectuée.

4. Fonction de perte - binary_crossentropy

loss= frac1N sum1N(ytrue cdotlog(ypred)+(1ytrue) cdotlog(1ypred))



5. Métrique de qualité du modèle - moyenne harmonique (mesure F)

F=2 fracPrecision timesRecallPrecision+Recall


Dans notre cas, la question des mesures de qualité n'est pas aussi importante que l'exactitude de l'importation. La justesse de l'importation est déterminée par la coïncidence des résultats dans les modèles NN Python et Java fonctionnant en mode Inférence.

Importez des modèles Keras dans DL4J:


Versions utilisées: Tensorflow 1.5.0 et Keras 2.2.5. Dans notre cas, le modèle de Python a été téléchargé dans son ensemble par le fichier HDF5.

# saving model model1.save('model1_functional.h5') 

Lors de l'importation d'un modèle dans DL4J, le module d'importation ne fournit pas de méthodes API pour transmettre des paramètres supplémentaires: le nom du module tensorflow (d'où les fonctions ont été importées lors de la construction du modèle).

De manière générale, DL4J ne fonctionne qu'avec les fonctions Keras, une liste exhaustive est donnée dans la section Keras Import [ 6 ], donc si le modèle a été créé sur Keras en utilisant des méthodes de TF (comme dans notre cas), le module d'importation ne pourra pas les identifier.

Consignes générales pour l'importation d'un modèle


De toute évidence, travailler avec le modèle Keras implique sa formation répétée. À cette fin, pour gagner du temps, des paramètres d'apprentissage ont été définis (1 époque) et 1 étape par époque (étapes_par_époque).

Lorsque vous importez un modèle pour la première fois, en particulier avec des couches personnalisées uniques et des combinaisons de couches rares, le succès est peu probable. Par conséquent, il est recommandé d'effectuer le processus d'importation de manière itérative: réduisez le nombre de couches du modèle Keras jusqu'à ce que vous puissiez importer et exécuter le modèle en Java sans erreurs. Ensuite, ajoutez une couche à la fois au modèle Keras et importez le modèle résultant en Java, en résolvant les erreurs qui se produisent.

Utilisation de la fonction de perte TF


Pour prouver que, lors de l'importation en Java, la fonction de perte du modèle entraîné doit provenir de Keras, nous utilisons log_loss de tensorflow (comme la plus similaire à la fonction custom_loss). Nous obtenons l'erreur suivante dans la console:

 Exception in thread "main" org.deeplearning4j.nn.modelimport.keras.exceptions.UnsupportedKerasConfigurationException: Unknown Keras loss function log_loss. 

Remplacement des méthodes TF par Keras


Dans notre cas, les fonctions du module TF sont utilisées 2 fois et dans tous les cas elles ne se trouvent que dans les couches lambda.

Les couches Lambda sont des couches personnalisées qui sont utilisées pour ajouter une fonction arbitraire.

Notre modèle n'a que 4 couches lambda. Le fait est qu'en Java, il est nécessaire d'enregistrer ces couches lambda manuellement via KerasLayer.registerLambdaLayer (sinon, nous obtiendrons une erreur [ 12 ]). Dans ce cas, la fonction définie à l'intérieur de la couche lambda doit être une fonction des bibliothèques Java correspondantes. En Java, il n'y a aucun exemple d'enregistrement de ces couches, ainsi qu'une documentation complète pour cela; un exemple est ici [ 13 ]. Des considérations générales ont été empruntées aux exemples [ 14 , 15 ].

Envisagez séquentiellement d'enregistrer toutes les couches lambda du modèle en Java:

1) Couche lambda pour ajouter des constantes au tenseur (matrice) un nombre fini de fois le long de directions données (dans notre cas, gauche et droite):

L'entrée de cette couche est connectée à l'entrée du modèle.

1.1) Couche Python:

 padding = keras.layers.Lambda(lambda x: tf.pad(x, paddings=[[0, 0], [10, 10]], constant_values=1))(embedding) 

Pour plus de clarté, les fonctions de cette couche fonctionnent, nous substituons explicitement les valeurs numériques dans les couches python.

Tableau avec un exemple d'un tenseur arbitraire 2x2
C'était 2x2Il est devenu 2x22
[[ 1 , 2 ],
[ 3 , 4 ]]
[[37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 1 , 2 , 37, 37, 37, 37, 37, 37, 37, 37, 37, 37],
[37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 3 , 4 , 37, 37, 37, 37, 37, 37, 37, 37, 37, 37]]


1.2) Couche 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); } }); 

Dans toutes les couches lambda enregistrées en Java, 2 fonctions sont redéfinies:
La première fonction «definelayer» est responsable de la méthode utilisée (pas du tout un fait évident: cette méthode ne peut être utilisée que sous nn () backend); getOutputType est responsable de la sortie de la couche enregistrée, l'argument est un paramètre numérique (ici 20, mais en général toute valeur entière est autorisée). Cela semble incohérent, mais cela fonctionne comme ceci.

2) Couche lambda pour couper le tenseur (matrice) dans des directions données (dans notre cas, gauche et droite):

Dans ce cas, la couche LSTM entre en entrée de la couche lambda.

2.1) Couche Python:

 slicing_lstm = keras.layers.Lambda(lambda x: x[:, 10:-10])(lstm) 

Tableau avec un exemple d'un tenseur arbitraire 2x22x5
C'était 2x22x5Il est devenu 2x2x5
[[[1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1 , 2,3,4,5], [1,2,3,4,5], [ 1 , 2 , 3 , 4 , 5 ], [ 1 , 2 , 3 , 4 , 5 ], [1,2 , 3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3 , 4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4 , 5], [1,2,3,4,5]],

[[1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [ 1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1, 2,3,4,5], [1,2,3,4,5], [ 1 , 2 , 3 , 4 , 5 ], [ 1 , 2 , 3 , 4 , 5 ], [1,2, 3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3, 4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4,5], [1,2,3,4, 5], [1,2,3,4,5]]]
[[[ 1 , 2 , 3 , 4 , 5 ], [ 1 , 2 , 3 , 4 , 5 ]],
[[ 1 , 2 , 3 , 4 , 5 ], [ 1 , 2 , 3 , 4 , 5 ]]]


2.2) Couche 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); } }); 

Dans le cas de cette couche, le paramètre InputType est passé de feedforward (20) à recurrent (60). Dans l'argument récurrent, le nombre peut être n'importe quel entier (différent de zéro), mais sa somme avec l'argument récurrent de la prochaine couche lambda doit donner 160 (c'est-à-dire que dans la couche suivante, l'argument doit être 100). Le nombre 160 est dû au fait que le tenseur de dimension (Aucun, Aucun, 160) doit être reçu à l'entrée concaténée_1 de la couche.

Les 2 premiers arguments sont des variables, selon la taille de la chaîne d'entrée.

3) Couche lambda pour tailler le tenseur (matrice) dans des directions données (dans notre cas, gauche et droite):

L'entrée de cette couche est la couche LSTM, devant laquelle se trouve la couche conv1_d

3.1) Couche Python:

 slicing_convolution = keras.layers.Lambda(lambda x: x[:,10:-10])(lstm_conv) 

Cette opération est totalement identique à l'opération du paragraphe 2.1.

3.2) Couche 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); } }); 

Cette couche lambda répète la couche lambda précédente à l'exception du paramètre récurrent (100). La raison pour laquelle "100" est pris est indiquée dans la description de la couche précédente.

Aux points 2 et 3, les couches lambda sont situées après les couches LSTM, donc le type récurrent est utilisé. Mais si avant la couche lambda il n'y avait pas LSTM, mais conv1d_1, alors il est toujours nécessaire de définir récurrent (il semble incohérent, mais cela fonctionne comme ça).

4) Couche lambda pour compresser la couche précédente:

L'entrée de cette couche est une couche entièrement connectée.

4.1) Couche Python:

  squeeze = keras.layers.Lambda(lambda x: tf.squeeze( x, axis=-1))(dense) 

Tableau avec un exemple d'un tenseur arbitraire 2x4x1
C'était 2x4x1Est devenu 2x4
[[[ [1], [2], [3], [4]] ,

[ [1], [2], [3], [4] ]]
[[ 1, 2, 3, 4 ],
[ 1, 2, 3, 4 ]]


4.2) Couche 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); } }); 

L'entrée de cette couche reçoit une couche entièrement connectée, InputType pour cette couche feedForward (15), le paramètre 15 n'affecte pas le modèle (toute valeur entière est autorisée).

Télécharger le modèle importé


Le modèle est chargé via le module ComputationGraph:

 ComputationGraph model = org.deeplearning4j.nn.modelimport.keras.KerasModelImport.importKerasModelAndWeights("/home/user/Models/model1_functional.h5"); 

Sortie de données vers la console Java


En Java, notamment en DL4J, les tenseurs sont écrits sous forme de tableaux issus de la bibliothèque Nd4j performante, qui peut être considérée comme un analogue de la bibliothèque Numpy en Python.

Supposons que notre chaîne d'entrée se compose de 4 caractères. Les symboles sont représentés sous forme d'entiers (sous forme d'indices), par exemple, selon une numérotation. Un tableau de la dimension correspondante (4) est créé pour eux.

Par exemple, nous avons 4 caractères codés en index: 1, 3, 4, 8.

Code en Java:

 INDArray myArray = Nd4j.zeros(1,4); // one row 4 column array myArray.putScalar(0,0,1); myArray.putScalar(0,1,3); myArray.putScalar(0,2,4); myArray.putScalar(0,3,8); INDArray output = model.outputSingle(myArray); System.out.println(output); 

La console affichera les probabilités pour chaque élément d'entrée.

Modèles importés


L'architecture du réseau neuronal d'origine et les poids sont importés sans erreur. Les modèles de réseau neuronal Keras et Java en mode Inférence sont d'accord sur les résultats.

Modèle Python:

image alt

Modèle Java:

image alt

En réalité, l'importation de modèles n'est pas si simple. Ci-dessous, nous soulignerons brièvement certains points qui peuvent dans certains cas être critiques.

1) La couche de normalisation des correctifs ne fonctionne pas après les couches récursives. Le problème est ouvert sur GitHub depuis près d'un an [ 16 ]. Par exemple, si vous ajoutez cette couche au modèle (après la couche de contact), nous obtenons l'erreur suivante:

 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 

Dans la pratique, le modèle a refusé de fonctionner, citant une erreur similaire lorsque la couche de normalisation a été ajoutée après conv1d. Après une couche entièrement connectée, l'ajout fonctionne parfaitement.

2) Après une couche entièrement connectée, la définition de la couche Aplatir entraîne une erreur. Une erreur similaire est mentionnée sur Stackoverflow [ 17 ]. Pendant six mois, aucun retour.

Ce n'est certainement pas toutes les restrictions que vous pouvez rencontrer lorsque vous travaillez avec DL4J.
La durée de fonctionnement finale du modèle est ici [ 18 ].

Conclusion


En conclusion, on peut noter que les modèles Keras formés importés sans douleur dans DL4J ne peuvent être que pour des cas simples (bien sûr, si vous n'avez pas une telle expérience, et même une bonne maîtrise de Java).

Moins il y a de couches d'utilisateurs, plus le modèle sera importé sans douleur, mais si l'architecture du réseau est complexe, vous devrez passer beaucoup de temps à le transférer vers DL4J.

Le support documentaire du module d'importation développé, le nombre d'exemples liés, semblait plutôt humide. À chaque étape, de nouvelles questions se posent - comment enregistrer les couches Lambda, signification des paramètres, etc.

Compte tenu de la vitesse de complexité des architectures de réseaux neuronaux et de l'interaction entre les couches, la complexité des couches, DL4J doit encore se développer activement afin d'atteindre le niveau des cadres haut de gamme pour travailler avec les réseaux de neurones artificiels.

En tout cas, les gars sont dignes de respect pour leur travail et aimeraient voir la poursuite du développement de cette direction.

Les références

  1. Top 5 des meilleurs langages de programmation pour le domaine de l'intelligence artificielle
  2. Scores de puissance du Deep Learning Framework 2018
  3. Comparaison de logiciels d'apprentissage en profondeur
  4. Top 9 des cadres dans le monde de l'intelligence artificielle
  5. DeepLearning4j. Modèles disponibles
  6. DeepLearning4j. Importation de modèle Keras. Fonctionnalités prises en charge.
  7. Deeplearning4j. Démarrage rapide
  8. Conférence 0: Premiers pas avec DeepLearning4j
  9. Deeplearing4j: importation de modèle Keras
  10. Conférence 7 | Importation de modèle Keras
  11. Installer TensorFlow pour Java
  12. Utilisation des couches Keras
  13. DeepLearning4j: Classe KerasLayer
  14. DeepLearning4j: SameDiffLambdaLayer.java
  15. DeepLearning4j: KerasLambdaTest.java
  16. DeepLearning4j: BatchNorm avec RecurrentInputType
  17. StackOverFlow: problème d'ouverture d'un modèle de kéros en java avec deeplearning4j (https://deeplearning4j.org/)
  18. GitHub: code complet pour le modèle en question
  19. Skymind: Comparaison des cadres AI

Source: https://habr.com/ru/post/fr475338/


All Articles