Déterminer la race d'un chien: un cycle de développement complet, d'un réseau neuronal en Python à une application sur Google Play

Les progrès dans le domaine des réseaux de neurones en général et de la reconnaissance des formes en particulier ont conduit au fait qu'il peut sembler que la création d'une application de réseau de neurones pour travailler avec des images est une tâche de routine. Dans un sens, c'est - si vous avez une idée liée à la reconnaissance des formes, ne doutez pas que quelqu'un a déjà écrit quelque chose comme ça. Il vous suffit de trouver le morceau de code correspondant dans Google et de le «compiler» à partir de l'auteur.

Cependant, il y a encore de nombreux détails qui rendent la tâche moins insoluble que ... ennuyeuse, je dirais. Cela prend trop de temps, surtout si vous êtes un débutant qui a besoin de leadership, étape par étape, d'un projet réalisé sous vos yeux et achevé du début à la fin. Sans l'habituel dans de tels cas, «sauter cette partie évidente» des excuses.

Dans cet article, nous examinerons la tâche de créer un identifiant de race de chien: nous allons créer et former un réseau de neurones, puis le porter sur Java pour Android et le publier sur Google Play.

Si vous voulez voir le résultat final, le voici: NeuroDog App sur Google Play.

Site Web avec ma robotique (en cours): robotics.snowcron.com .
Site Web avec le programme lui-même, y compris un guide: NeuroDog User Guide .

Et voici une capture d'écran du programme:

image



Énoncé du problème



Nous utiliserons Keras: une bibliothèque Google pour travailler avec les réseaux de neurones. Il s'agit d'une bibliothèque de haut niveau, ce qui signifie qu'elle est plus facile à utiliser que les alternatives que je connais. Si quoi que ce soit - il existe de nombreux manuels sur Keras dans le réseau, de haute qualité.

Nous utiliserons CNN - Convolutional Neural Networks. CNN (et les configurations plus avancées basées sur eux) sont la norme de facto en matière de reconnaissance d'image. Dans le même temps, la formation d'un tel réseau n'est pas toujours facile: vous devez choisir la bonne structure de réseau, les paramètres de formation (tous ces taux d'apprentissage, l'élan, L1 et L2, etc.). La tâche nécessite des ressources informatiques importantes, et donc, pour la résoudre simplement en passant par TOUS les paramètres échouera.

C'est l'une des nombreuses raisons pour lesquelles, dans la plupart des cas, ils utilisent ce que l'on appelle le «transfert de connaissances» au lieu de l'approche dite «vanille». Transfer Knowlege utilise un réseau de neurones formé par quelqu'un avant nous (par exemple, Google) et généralement pour une tâche similaire, mais toujours différente. Nous en prenons les couches initiales, remplaçons les couches finales par notre propre classificateur - et cela fonctionne, et cela fonctionne très bien.

Au début, un tel résultat peut être surprenant: comment se fait-il que nous ayons pris un réseau Google formé pour distinguer les chats des chaises, et qu'il reconnaisse les races de chiens pour nous? Pour comprendre comment cela se produit, vous devez comprendre les principes de base du travail des réseaux de neurones profonds, y compris ceux utilisés pour la reconnaissance des formes.

Nous avons «alimenté» le réseau en image (un tableau de nombres, c'est-à-dire) en entrée. La première couche analyse l'image pour des motifs simples, tels que «ligne horizontale», «arc», etc. La couche suivante reçoit ces motifs en entrée et produit des motifs de second ordre, tels que «fourrure», «coin de l'œil» ... En fin de compte, nous obtenons un puzzle à partir duquel nous pouvons reconstruire le chien: laine, deux yeux et une main humaine en dents.

Tout ce qui précède a été fait à l'aide de couches pré-formées obtenues par nous (par exemple, de Google). Ensuite, nous ajoutons nos couches et leur apprenons à extraire des informations sur la race de ces modèles. Cela semble logique.

Pour résumer, dans cet article, nous allons créer à la fois CNN «vanille» et plusieurs variantes «transfert d'apprentissage» de différents types de réseaux. Quant à «vanilla»: je vais le créer, mais je ne prévois pas de le configurer en sélectionnant des paramètres, car il est beaucoup plus facile de former et de configurer des réseaux «pré-formés».

Puisque nous prévoyons d'enseigner à notre réseau de neurones à reconnaître les races de chiens, nous devons lui «montrer» des échantillons de diverses races. Heureusement, il existe un ensemble de photographies créées ici pour une tâche similaire (l' original est ici ).

Ensuite, je prévois de porter le meilleur des réseaux reçus pour Android. Le portage des réseaux Kerasov sur Android est relativement simple, bien formalisé et nous ferons toutes les étapes nécessaires, il ne sera donc pas difficile de reproduire cette partie.

Ensuite, nous publierons tout cela sur Google Play. Naturellement, Google résistera, donc des astuces supplémentaires seront utilisées. Par exemple, la taille de notre application (en raison d'un réseau de neurones volumineux) sera supérieure à la taille autorisée de l'APK Android acceptée par Google Play: nous devrons utiliser des bundles. De plus, Google n'affichera pas notre application dans les résultats de recherche, cela peut être corrigé en enregistrant des balises de recherche dans l'application, ou attendez simplement ... une semaine ou deux.

En conséquence, nous obtenons une application "commerciale" entièrement fonctionnelle (entre guillemets, car elle est présentée gratuitement) pour Android et utilisant des réseaux de neurones.

Environnement de développement



Vous pouvez programmer Keras de différentes manières, en fonction du système d'exploitation que vous utilisez (Ubuntu recommandé), de la présence ou de l'absence d'une carte vidéo, etc. Il n'y a rien de mal dans le développement sur l'ordinateur local (et, par conséquent, sa configuration), sauf que ce n'est pas le moyen le plus simple.

Tout d'abord, l'installation et la configuration d'un grand nombre d'outils et de bibliothèques prennent du temps, puis lorsque de nouvelles versions seront publiées, vous devrez passer du temps à nouveau. Deuxièmement, les réseaux de neurones nécessitent une grande puissance de calcul pour la formation. Vous pouvez accélérer (de 10 fois ou plus) ce processus si vous utilisez un GPU ... au moment de la rédaction de cet article, les meilleurs GPU les plus appropriés pour ce travail coûtaient entre 2000 $ et 7000 $. Et oui, ils doivent également être configurés.

Nous allons donc aller dans l'autre sens. Le fait est que Google permet aux hérissons pauvres comme nous d'utiliser les GPU de leur cluster - gratuitement, pour les calculs liés aux réseaux de neurones, il fournit également un environnement entièrement configuré, tous ensemble, cela s'appelle Google Colab. Le service vous donne accès à Jupiter Notebook avec python, Keras et un grand nombre d'autres bibliothèques déjà configurées. Tout ce que vous avez à faire est d'obtenir un compte Google (obtenez un compte Gmail et cela vous donnera accès à tout le reste).

Pour le moment, Colab peut être embauché ici , mais connaissant Google, cela peut changer à tout moment. Il suffit de google Google Colab.

Le problème évident avec l'utilisation de Colab est qu'il s'agit d'un service WEB. Comment accédons-nous à nos données? Enregistrer le réseau neuronal après la formation, par exemple, télécharger des données spécifiques à notre tâche, etc.?

Il existe plusieurs (au moment de la rédaction de cet article - trois) différentes manières, nous utilisons celle qui me semble la plus pratique - nous utilisons Google Drive.

Google Drive est un stockage de données basé sur le cloud qui fonctionne un peu comme un disque dur ordinaire, et il peut être mappé sur Google Colab (voir le code ci-dessous). Après cela, vous pouvez travailler avec lui comme vous le feriez avec des fichiers sur un disque local. C'est-à-dire, par exemple, afin d'accéder aux photos de chiens pour former notre réseau de neurones, nous devons les télécharger sur Google Drive, c'est tout.

Création et formation d'un réseau neuronal



Ci-dessous, je donne le code en Python, bloc par bloc (du Jupiter Notebook). Vous pouvez copier ce code dans votre bloc-notes Jupiter et l'exécuter également, bloc par bloc, car les blocs peuvent être exécutés indépendamment (bien sûr, les variables définies dans le premier bloc peuvent être requises à la fin, mais il s'agit d'une dépendance évidente).

Initialisation



Tout d'abord, montons Google Drive. Seulement deux lignes. Ce code ne doit être exécuté qu'une seule fois dans une session Colab (disons une fois toutes les 6 heures). Si vous l'appelez une deuxième fois alors que la session est encore «vivante», elle sera ignorée car le lecteur est déjà monté.

from google.colab import drive drive.mount('/content/drive/') 


Au premier démarrage, il vous sera demandé de confirmer vos intentions, il n'y a rien de compliqué. Voici à quoi ça ressemble:

 >>> Go to this URL in a browser: ... >>> Enter your authorization code: >>> ·········· >>> Mounted at /content/drive/ 


Une section d' inclusion complètement standard; il est possible que certains des fichiers inclus ne soient pas nécessaires, eh bien ... désolé. De plus, comme je vais tester différents réseaux de neurones, vous devrez commenter / décommenter certains des modules inclus pour des types spécifiques de réseaux de neurones: par exemple, pour utiliser InceptionV3 NN, décommenter l'inclusion d'InceptionV3 et commenter, par exemple, ResNet50. Ou pas: tout ce qui change, c'est la taille de la mémoire utilisée, et ce n'est pas très fort.

 import datetime as dt import pandas as pd import seaborn as sns import matplotlib.pyplot as plt from tqdm import tqdm import cv2 import numpy as np import os import sys import random import warnings from sklearn.model_selection import train_test_split import keras from keras import backend as K from keras import regularizers from keras.models import Sequential from keras.models import Model from keras.layers import Dense, Dropout, Activation from keras.layers import Flatten, Conv2D from keras.layers import MaxPooling2D from keras.layers import BatchNormalization, Input from keras.layers import Dropout, GlobalAveragePooling2D from keras.callbacks import Callback, EarlyStopping from keras.callbacks import ReduceLROnPlateau from keras.callbacks import ModelCheckpoint import shutil from keras.applications.vgg16 import preprocess_input from keras.preprocessing import image from keras.preprocessing.image import ImageDataGenerator from keras.models import load_model from keras.applications.resnet50 import ResNet50 from keras.applications.resnet50 import preprocess_input from keras.applications.resnet50 import decode_predictions from keras.applications import inception_v3 from keras.applications.inception_v3 import InceptionV3 from keras.applications.inception_v3 import preprocess_input as inception_v3_preprocessor from keras.applications.mobilenetv2 import MobileNetV2 from keras.applications.nasnet import NASNetMobile 


Sur Google Drive, nous créons un dossier pour nos fichiers. La deuxième ligne affiche son contenu:

 working_path = "/content/drive/My Drive/DeepDogBreed/data/" !ls "/content/drive/My Drive/DeepDogBreed/data" >>> all_images labels.csv models test train valid 


Comme vous pouvez le voir, les photos des chiens (copiées à partir du jeu de données Stanford (voir ci-dessus) sur Google Drive) sont d'abord enregistrées dans le dossier all_images . Plus tard, nous les copierons dans les répertoires train, valid et test . Nous enregistrerons les modèles formés dans le dossier des modèles . Quant au fichier labels.csv, il fait partie du jeu de données avec photos, il contient un tableau de correspondance des noms des photos et des races de chiens.

Il existe de nombreux tests que vous pouvez exécuter pour comprendre exactement ce que nous avons obtenu pour une utilisation temporaire de Google. Par exemple:

 # Is GPU Working? import tensorflow as tf tf.test.gpu_device_name() >>> '/device:GPU:0' 


Comme vous pouvez le voir, le GPU est vraiment connecté, et sinon, vous devez trouver et activer cette option dans les paramètres de Jupiter Notebook.

Ensuite, nous devons déclarer certaines constantes, telles que la taille des images, etc. Nous utiliserons des images d'une taille de 256x256 pixels, c'est une image assez grande pour ne pas perdre de détails, et assez petite pour que tout rentre dans la mémoire. Notez, cependant, que certains types de réseaux de neurones que nous utiliserons attendent des images de 224x224 pixels. Dans de tels cas, nous commentons 256 et décommentons 224.

La même approche (commentaire 1 - décommentation) sera appliquée aux noms des modèles que nous enregistrons, tout simplement parce que nous ne voulons pas écraser les fichiers qui peuvent encore être utiles.
 warnings.filterwarnings("ignore") os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' np.random.seed(7) start = dt.datetime.now() BATCH_SIZE = 16 EPOCHS = 15 TESTING_SPLIT=0.3 # 70/30 % NUM_CLASSES = 120 IMAGE_SIZE = 256 #strModelFileName = "models/ResNet50.h5" # strModelFileName = "models/InceptionV3.h5" strModelFileName = "models/InceptionV3_Sgd.h5" #IMAGE_SIZE = 224 #strModelFileName = "models/MobileNetV2.h5" #IMAGE_SIZE = 224 #strModelFileName = "models/NASNetMobileSgd.h5" 


Chargement des données



Tout d'abord, téléchargeons le fichier labels.csv et divisons- le en parties de formation et de validation. Notez qu'il n'y a pas encore de partie test, car je vais tricher pour obtenir plus de données d'entraînement.

 labels = pd.read_csv(working_path + 'labels.csv') print(labels.head()) train_ids, valid_ids = train_test_split(labels, test_size = TESTING_SPLIT) print(len(train_ids), 'train ids', len(valid_ids), 'validation ids') print('Total', len(labels), 'testing images') >>> id breed >>> 0 000bec180eb18c7604dcecc8fe0dba07 boston_bull >>> 1 001513dfcb2ffafc82cccf4d8bbaba97 dingo >>> 2 001cdf01b096e06d78e9e5112d419397 pekinese >>> 3 00214f311d5d2247d5dfe4fe24b2303d bluetick >>> 4 0021f9ceb3235effd7fcde7f7538ed62 golden_retriever >>> 7155 train ids 3067 validation ids >>> Total 10222 testing images 


Ensuite, copiez les fichiers image dans les dossiers de formation / validation / test, selon les noms de fichiers. La fonction suivante copie les fichiers dont nous transférons les noms dans le dossier spécifié.

 def copyFileSet(strDirFrom, strDirTo, arrFileNames): arrBreeds = np.asarray(arrFileNames['breed']) arrFileNames = np.asarray(arrFileNames['id']) if not os.path.exists(strDirTo): os.makedirs(strDirTo) for i in tqdm(range(len(arrFileNames))): strFileNameFrom = strDirFrom + arrFileNames[i] + ".jpg" strFileNameTo = strDirTo + arrBreeds[i] + "/" + arrFileNames[i] + ".jpg" if not os.path.exists(strDirTo + arrBreeds[i] + "/"): os.makedirs(strDirTo + arrBreeds[i] + "/") # As a new breed dir is created, copy 1st file # to "test" under name of that breed if not os.path.exists(working_path + "test/"): os.makedirs(working_path + "test/") strFileNameTo = working_path + "test/" + arrBreeds[i] + ".jpg" shutil.copy(strFileNameFrom, strFileNameTo) shutil.copy(strFileNameFrom, strFileNameTo) 


Comme vous pouvez le voir, nous ne copions qu'un fichier pour chaque race de chien comme test . De plus, lors de la copie, nous créons des sous-dossiers, un pour chaque race. Par conséquent, les photographies sont copiées dans des sous-dossiers par race.

Cela est dû au fait que Keras peut travailler avec un répertoire d'une structure similaire, en chargeant des fichiers image selon les besoins, et pas tous en même temps, ce qui économise de la mémoire. Télécharger les 15 000 images à la fois est une mauvaise idée.

Nous devrons appeler cette fonction une seule fois, car elle copie des images - et n'est plus nécessaire. En conséquence, pour une utilisation future, nous devons le commenter:

 # Move the data in subfolders so we can # use the Keras ImageDataGenerator. # This way we can also later use Keras # Data augmentation features. # --- Uncomment once, to copy files --- #copyFileSet(working_path + "all_images/", # working_path + "train/", train_ids) #copyFileSet(working_path + "all_images/", # working_path + "valid/", valid_ids) 


Obtenez une liste des races de chiens:

 breeds = np.unique(labels['breed']) map_characters = {} #{0:'none'} for i in range(len(breeds)): map_characters[i] = breeds[i] print("<item>" + breeds[i] + "</item>") >>> <item>affenpinscher</item> >>> <item>afghan_hound</item> >>> <item>african_hunting_dog</item> >>> <item>airedale</item> >>> <item>american_staffordshire_terrier</item> >>> <item>appenzeller</item> 


Traitement d'image



Nous allons utiliser la fonctionnalité de bibliothèque Keras appelée ImageDataGenerators. ImageDataGenerator peut traiter l'image, la mettre à l'échelle, la faire pivoter, etc. Il peut également accepter une fonction de traitement qui peut traiter des images en plus.

 def preprocess(img): img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE), interpolation = cv2.INTER_AREA) # or use ImageDataGenerator( rescale=1./255... img_1 = image.img_to_array(img) img_1 = cv2.resize(img_1, (IMAGE_SIZE, IMAGE_SIZE), interpolation = cv2.INTER_AREA) img_1 = np.expand_dims(img_1, axis=0) / 255. #img = cv2.blur(img,(5,5)) return img_1[0] 


Faites attention au code suivant:

 # or use ImageDataGenerator( rescale=1./255... 


Nous pouvons normaliser (sous-données sous la plage 0-1 au lieu du 0-255 d'origine) dans ImageDataGenerator lui-même. Pourquoi alors avons-nous besoin d'un préprocesseur? À titre d'exemple, considérons l'appel flou (commenté, je ne l'utilise pas): il s'agit de la même manipulation d'image personnalisée qui peut être arbitraire. Tout, du contraste au HDR.

Nous utiliserons deux ImageDataGenerators différents, un pour la formation et un pour la validation. La différence est que pour la formation, nous avons besoin de tours et de mise à l'échelle pour augmenter la «variété» de données, mais pour la validation, nous n'en avons pas besoin, du moins pas dans cette tâche.

 train_datagen = ImageDataGenerator( preprocessing_function=preprocess, #rescale=1./255, # done in preprocess() # randomly rotate images (degrees, 0 to 30) rotation_range=30, # randomly shift images horizontally # (fraction of total width) width_shift_range=0.3, height_shift_range=0.3, # randomly flip images horizontal_flip=True, ,vertical_flip=False, zoom_range=0.3) val_datagen = ImageDataGenerator( preprocessing_function=preprocess) train_gen = train_datagen.flow_from_directory( working_path + "train/", batch_size=BATCH_SIZE, target_size=(IMAGE_SIZE, IMAGE_SIZE), shuffle=True, class_mode="categorical") val_gen = val_datagen.flow_from_directory( working_path + "valid/", batch_size=BATCH_SIZE, target_size=(IMAGE_SIZE, IMAGE_SIZE), shuffle=True, class_mode="categorical") 


Création d'un réseau de neurones



Comme déjà mentionné, nous allons créer plusieurs types de réseaux de neurones. Chaque fois, nous appellerons une autre fonction pour créer, inclure d'autres fichiers et parfois déterminer une taille d'image différente. Ainsi, pour basculer entre différents types de réseaux de neurones, nous devons commenter / décommenter le code approprié.

Tout d'abord, créez un CNN «vanille». Cela ne fonctionne pas bien, car j'ai décidé de ne pas perdre de temps à le déboguer, mais au moins cela fournit une base qui peut être développée s'il y a un désir (généralement c'est une mauvaise idée, car les réseaux pré-formés donnent le meilleur résultat).

 def createModelVanilla(): model = Sequential() # Note the (7, 7) here. This is one of technics # used to reduce memory use by the NN: we scan # the image in a larger steps. # Also note regularizers.l2: this technic is # used to prevent overfitting. The "0.001" here # is an empirical value and can be optimized. model.add(Conv2D(16, (7, 7), padding='same', use_bias=False, input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3), kernel_regularizer=regularizers.l2(0.001))) # Note the use of a standard CNN building blocks: # Conv2D - BatchNormalization - Activation # MaxPooling2D - Dropout # The last two are used to avoid overfitting, also, # MaxPooling2D reduces memory use. model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2), padding='same')) model.add(Dropout(0.5)) model.add(Conv2D(16, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='same')) model.add(Dropout(0.5)) model.add(Conv2D(32, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(Dropout(0.5)) model.add(Conv2D(32, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='same')) model.add(Dropout(0.5)) model.add(Conv2D(64, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(Dropout(0.5)) model.add(Conv2D(64, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='same')) model.add(Dropout(0.5)) model.add(Conv2D(128, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(Dropout(0.5)) model.add(Conv2D(128, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='same')) model.add(Dropout(0.5)) model.add(Conv2D(256, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(Dropout(0.5)) model.add(Conv2D(256, (3, 3), padding='same', use_bias=False, kernel_regularizer=regularizers.l2(0.01))) model.add(BatchNormalization(axis=3, scale=False)) model.add(Activation("relu")) model.add(MaxPooling2D(pool_size=(2, 2), strides=(1, 1), padding='same')) model.add(Dropout(0.5)) # This is the end on "convolutional" part of CNN. # Now we need to transform multidementional # data into one-dim. array for a fully-connected # classifier: model.add(Flatten()) # And two layers of classifier itself (plus an # Activation layer in between): model.add(Dense(NUM_CLASSES, activation='softmax', kernel_regularizer=regularizers.l2(0.01))) model.add(Activation("relu")) model.add(Dense(NUM_CLASSES, activation='softmax', kernel_regularizer=regularizers.l2(0.01))) # We need to compile the resulting network. # Note that there are few parameters we can # try here: the best performing one is uncommented, # the rest is commented out for your reference. #model.compile(optimizer='rmsprop', # loss='categorical_crossentropy', # metrics=['accuracy']) #model.compile( # optimizer=keras.optimizers.RMSprop(lr=0.0005), # loss='categorical_crossentropy', # metrics=['accuracy']) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) #model.compile(optimizer='adadelta', # loss='categorical_crossentropy', # metrics=['accuracy']) #opt = keras.optimizers.Adadelta(lr=1.0, # rho=0.95, epsilon=0.01, decay=0.01) #model.compile(optimizer=opt, # loss='categorical_crossentropy', # metrics=['accuracy']) #opt = keras.optimizers.RMSprop(lr=0.0005, # rho=0.9, epsilon=None, decay=0.0001) #model.compile(optimizer=opt, # loss='categorical_crossentropy', # metrics=['accuracy']) # model.summary() return(model) 


Lorsque nous créons des réseaux à l'aide de l' apprentissage par transfert , la procédure change:

 def createModelMobileNetV2(): # First, create the NN and load pre-trained # weights for it ('imagenet') # Note that we are not loading last layers of # the network (include_top=False), as we are # going to add layers of our own: base_model = MobileNetV2(weights='imagenet', include_top=False, pooling='avg', input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)) # Then attach our layers at the end. These are # to build "classifier" that makes sense of # the patterns previous layers provide: x = base_model.output x = Dense(512)(x) x = Activation('relu')(x) x = Dropout(0.5)(x) predictions = Dense(NUM_CLASSES, activation='softmax')(x) # Create a model model = Model(inputs=base_model.input, outputs=predictions) # We need to make sure that pre-trained # layers are not changed when we train # our classifier: # Either this: #model.layers[0].trainable = False # or that: for layer in base_model.layers: layer.trainable = False # As always, there are different possible # settings, I tried few and chose the best: # model.compile(optimizer='adam', # loss='categorical_crossentropy', # metrics=['accuracy']) model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) #model.summary() return(model) 


La création d'autres types de réseaux suit le même schéma:

 def createModelResNet50(): base_model = ResNet50(weights='imagenet', include_top=False, pooling='avg', input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)) x = base_model.output x = Dense(512)(x) x = Activation('relu')(x) x = Dropout(0.5)(x) predictions = Dense(NUM_CLASSES, activation='softmax')(x) model = Model(inputs=base_model.input, outputs=predictions) #model.layers[0].trainable = False # model.compile(loss='categorical_crossentropy', # optimizer='adam', metrics=['accuracy']) model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) #model.summary() return(model) 


Attention: gagnant! Ce NN a montré le meilleur résultat:

 def createModelInceptionV3(): # model.layers[0].trainable = False # model.compile(optimizer='sgd', # loss='categorical_crossentropy', # metrics=['accuracy']) base_model = InceptionV3(weights = 'imagenet', include_top = False, input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)) x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(512, activation='relu')(x) predictions = Dense(NUM_CLASSES, activation='softmax')(x) model = Model(inputs = base_model.input, outputs = predictions) for layer in base_model.layers: layer.trainable = False # model.compile(optimizer='adam', # loss='categorical_crossentropy', # metrics=['accuracy']) model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) #model.summary() return(model) 


Un autre:

 def createModelNASNetMobile(): # model.layers[0].trainable = False # model.compile(optimizer='sgd', # loss='categorical_crossentropy', # metrics=['accuracy']) base_model = NASNetMobile(weights = 'imagenet', include_top = False, input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3)) x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(512, activation='relu')(x) predictions = Dense(NUM_CLASSES, activation='softmax')(x) model = Model(inputs = base_model.input, outputs = predictions) for layer in base_model.layers: layer.trainable = False # model.compile(optimizer='adam', # loss='categorical_crossentropy', # metrics=['accuracy']) model.compile(optimizer='sgd', loss='categorical_crossentropy', metrics=['accuracy']) #model.summary() return(model) 


Différents types de réseaux de neurones peuvent être utilisés pour différentes tâches. Ainsi, en plus des exigences de précision des prédictions, la taille peut avoir de l'importance (le NN mobile est 5 fois plus petit que Inception) et la vitesse (si nous avons besoin du traitement en temps réel d'un flux vidéo, alors la précision devra être sacrifiée).

Formation au réseau de neurones



Tout d'abord, nous expérimentons , nous devrions donc pouvoir supprimer les réseaux de neurones que nous avons enregistrés, mais que nous n'utilisons plus. La fonction suivante supprime NN s'il existe:

 # Make sure that previous "best network" is deleted. def deleteSavedNet(best_weights_filepath): if(os.path.isfile(best_weights_filepath)): os.remove(best_weights_filepath) print("deleteSavedNet():File removed") else: print("deleteSavedNet():No file to remove") 


La façon dont nous créons et supprimons les réseaux de neurones est assez simple et directe. Tout d'abord, supprimez. Lorsque vous appelez delete (uniquement), il convient de garder à l'esprit que le Jupiter Notebook a une fonction «exécuter la sélection», sélectionnez uniquement ce que vous souhaitez utiliser et exécutez-le.

Ensuite, nous créons un réseau neuronal si son fichier n'existait pas, ou appelons load s'il existe: bien sûr, nous ne pouvons pas appeler «delete» et nous attendre à ce que NN existe, donc pour utiliser un réseau neuronal enregistré, n'appelez pas delete .

En d'autres termes, nous pouvons créer un nouveau NN, ou utiliser celui existant, en fonction de la situation et de ce que nous expérimentons actuellement. Un scénario simple: nous avons formé un réseau de neurones, puis sommes partis en vacances. Ils sont revenus et Google a cloué la session, nous devons donc charger celle précédemment enregistrée: commenter «supprimer» et décommenter «charger».

 deleteSavedNet(working_path + strModelFileName) #if not os.path.exists(working_path + "models"): # os.makedirs(working_path + "models") # #if not os.path.exists(working_path + # strModelFileName): # model = createModelResNet50() model = createModelInceptionV3() # model = createModelMobileNetV2() # model = createModelNASNetMobile() #else: # model = load_model(working_path + strModelFileName) 


Les points de contrôle sont un élément très important de notre programme. Nous pouvons créer un tableau de fonctions qui devraient être appelées à la fin de chaque ère de formation et le transmettre au point de contrôle. Par exemple, vous pouvez enregistrer un réseau de neurones s'il affiche des résultats meilleurs que ceux déjà enregistrés.

 checkpoint = ModelCheckpoint(working_path + strModelFileName, monitor='val_acc', verbose=1, save_best_only=True, mode='auto', save_weights_only=False) callbacks_list = [ checkpoint ] 


Enfin, nous enseignons le réseau neuronal sur l'ensemble d'entraînement:

 # Calculate sizes of training and validation sets STEP_SIZE_TRAIN=train_gen.n//train_gen.batch_size STEP_SIZE_VALID=val_gen.n//val_gen.batch_size # Set to False if we are experimenting with # some other part of code, use history that # was calculated before (and is still in # memory bDoTraining = True if bDoTraining == True: # model.fit_generator does the actual training # Note the use of generators and callbacks # that were defined earlier history = model.fit_generator(generator=train_gen, steps_per_epoch=STEP_SIZE_TRAIN, validation_data=val_gen, validation_steps=STEP_SIZE_VALID, epochs=EPOCHS, callbacks=callbacks_list) # --- After fitting, load the best model # This is important as otherwise we'll # have the LAST model loaded, not necessarily # the best one. model.load_weights(working_path + strModelFileName) # --- Presentation part # summarize history for accuracy plt.plot(history.history['acc']) plt.plot(history.history['val_acc']) plt.title('model accuracy') plt.ylabel('accuracy') plt.xlabel('epoch') plt.legend(['acc', 'val_acc'], loc='upper left') plt.show() # summarize history for loss plt.plot(history.history['loss']) plt.plot(history.history['val_loss']) plt.title('model loss') plt.ylabel('loss') plt.xlabel('epoch') plt.legend(['loss', 'val_loss'], loc='upper left') plt.show() # As grid optimization of NN would take too long, # I did just few tests with different parameters. # Below I keep results, commented out, in the same # code. As you can see, Inception shows the best # results: # Inception: # adam: val_acc 0.79393 # sgd: val_acc 0.80892 # Mobile: # adam: val_acc 0.65290 # sgd: Epoch 00015: val_acc improved from 0.67584 to 0.68469 # sgd-30 epochs: 0.68 # NASNetMobile, adam: val_acc did not improve from 0.78335 # NASNetMobile, sgd: 0.8 


Les graphiques de précision et de perte pour la meilleure des configurations sont les suivants:




Comme vous pouvez le voir, le réseau neuronal apprend, et pas mal.

Test de réseau neuronal



Une fois la formation terminée, nous devons tester le résultat; pour cela, NN présente des photos qu'elle n'avait jamais vues auparavant - celles que nous avons copiées dans le dossier de test - une pour chaque race de chien.

 # --- Test j = 0 # Final cycle performs testing on the entire # testing set. for file_name in os.listdir( working_path + "test/"): img = image.load_img(working_path + "test/" + file_name); img_1 = image.img_to_array(img) img_1 = cv2.resize(img_1, (IMAGE_SIZE, IMAGE_SIZE), interpolation = cv2.INTER_AREA) img_1 = np.expand_dims(img_1, axis=0) / 255. y_pred = model.predict_on_batch(img_1) # get 5 best predictions y_pred_ids = y_pred[0].argsort()[-5:][::-1] print(file_name) for i in range(len(y_pred_ids)): print("\n\t" + map_characters[y_pred_ids[i]] + " (" + str(y_pred[0][y_pred_ids[i]]) + ")") print("--------------------\n") j = j + 1 


Exporter un réseau de neurones vers une application Java



Tout d'abord, nous devons organiser le chargement du réseau neuronal à partir du disque. La raison est claire: l'exportation a lieu dans un autre bloc de code, donc nous commencerons probablement l'exportation séparément - lorsque le réseau de neurones sera amené à son état optimal. Autrement dit, immédiatement avant l'exportation, dans le même cycle du programme, nous ne formerons pas le réseau. Si vous utilisez le code affiché ici, il n'y a aucune différence, le réseau optimal a été sélectionné pour vous. Mais si vous apprenez quelque chose par vous-même, alors tout réentraîner avant de sauver est une perte de temps, si auparavant vous avez tout sauvé.

 # Test: load and run model = load_model(working_path + strModelFileName) 


Pour la même raison - pour ne pas sauter par dessus le code - j'inclus ici les fichiers nécessaires à l'exportation. Personne ne vous dérange pour les déplacer au début du programme si votre sens de la beauté l'exige:

 from keras.models import Model from keras.models import load_model from keras.layers import * import os import sys import tensorflow as tf 


Un petit test après le chargement d'un réseau de neurones, juste pour vous assurer que tout est chargé - fonctionne:

 img = image.load_img(working_path + "test/affenpinscher.jpg") #basset.jpg") img_1 = image.img_to_array(img) img_1 = cv2.resize(img_1, (IMAGE_SIZE, IMAGE_SIZE), interpolation = cv2.INTER_AREA) img_1 = np.expand_dims(img_1, axis=0) / 255. y_pred = model.predict(img_1) Y_pred_classes = np.argmax(y_pred,axis = 1) # print(y_pred) fig, ax = plt.subplots() ax.imshow(img) ax.axis('off') ax.set_title(map_characters[Y_pred_classes[0]]) plt.show() 


image

Ensuite, nous devons obtenir les noms des couches d'entrée et de sortie du réseau (que ce soit la fonction de création, nous devons explicitement «nommer» les couches, ce que nous n'avons pas fait).

 model.summary() >>> Layer (type) >>> ====================== >>> input_7 (InputLayer) >>> ______________________ >>> conv2d_283 (Conv2D) >>> ______________________ >>> ... >>> dense_14 (Dense) >>> ====================== >>> Total params: 22,913,432 >>> Trainable params: 1,110,648 >>> Non-trainable params: 21,802,784 


Nous utiliserons les noms des couches d'entrée et de sortie ultérieurement lors de l'importation du réseau neuronal dans une application Java.

Un autre code itinérant sur le réseau pour obtenir ces données:

 def print_graph_nodes(filename): g = tf.GraphDef() g.ParseFromString(open(filename, 'rb').read()) print() print(filename) print("=======================INPUT===================") print([n for n in g.node if n.name.find('input') != -1]) print("=======================OUTPUT==================") print([n for n in g.node if n.name.find('output') != -1]) print("===================KERAS_LEARNING==============") print([n for n in g.node if n.name.find('keras_learning_phase') != -1]) print("===============================================") print() #def get_script_path(): # return os.path.dirname(os.path.realpath(sys.argv[0])) 


.

Keras Neural Network pb , , Android.

 def keras_to_tensorflow(keras_model, output_dir, model_name,out_prefix="output_", log_tensorboard=True): if os.path.exists(output_dir) == False: os.mkdir(output_dir) out_nodes = [] for i in range(len(keras_model.outputs)): out_nodes.append(out_prefix + str(i + 1)) tf.identity(keras_model.output[i], out_prefix + str(i + 1)) sess = K.get_session() from tensorflow.python.framework import graph_util from tensorflow.python.framework graph_io init_graph = sess.graph.as_graph_def() main_graph = graph_util.convert_variables_to_constants( sess, init_graph, out_nodes) graph_io.write_graph(main_graph, output_dir, name=model_name, as_text=False) if log_tensorboard: from tensorflow.python.tools import import_pb_to_tensorboard import_pb_to_tensorboard.import_to_tensorboard( os.path.join(output_dir, model_name), output_dir) 


:

 model = load_model(working_path + strModelFileName) keras_to_tensorflow(model, output_dir=working_path + strModelFileName, model_name=working_path + "models/dogs.pb") print_graph_nodes(working_path + "models/dogs.pb") 


.

Android



Android . , , , ( ) .

, Android Studio . « », — . activity.

image

, «assets» (, ).

Gradle



. , tensorflow-android . , Tensorflow (, , Keras) Java:

image

: versionCode versionName . , Google Play. gdadle (, 1 -> 2 -> 3...) , « ».



, «» — 100 Mb Neural Network , instance «» Facebook .

instance :

 <activity android:name=".MainActivity" android:launchMode="singleTask"> 


android:launchMode=«singleTask» MainActivity, Android, () , , instance.

, , - «» :

 <intent-filter> <!-- Send action required to display activity in share list --> <action android:name="android.intent.action.SEND" /> <!-- Make activity default to launch --> <category android:name="android.intent.category.DEFAULT" /> <!-- Mime type ie what can be shared with this activity only image and text --> <data android:mimeType="image/*" /> </intent-filter> 


, , :

 <uses-feature android:name="android.hardware.camera" android:required="true" /> <uses-permission android:name= "android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" /> 


Android, .

Layout .



layouts, , . Portrait layout .

: (view) , (, «»), «Help», File/Gallery , ( ) «Process» .

image

, enabling/disabling , .

MainActivity



activity (extends) Android Activity:

 public class MainActivity extends Activity 


, .

, Bitmap. , Bitmap ( ) (m_bitmap), , 256x256 (m_bitmapForNn). (256) :

 static Bitmap m_bitmap = null; static Bitmap m_bitmapForNn = null; private int m_nImageSize = 256; 


; (. ), , :

 private String INPUT_NAME = "input_7_1"; private String OUTPUT_NAME = "output_1"; 


TensofFlow. , ( assets):

private TensorFlowInferenceInterface tf;
private String MODEL_PATH = 
	"file:///android_asset/dogs.pb";


, , :
 private String[] m_arrBreedsArray; 


, Bitmap. , RGB , — , — , . , (, 120 , ):

 private float[] m_arrPrediction = new float[120]; private float[] m_arrInput = null; 


tensorflow inference library:

 static { System.loadLibrary("tensorflow_inference"); } 


, , , « », .

 class PredictionTask extends AsyncTask<Void, Void, Void> { @Override protected void onPreExecute() { super.onPreExecute(); } // --- @Override protected Void doInBackground(Void... params) { try { # We get RGB values packed in integers # from the Bitmap, then break those # integers into individual triplets m_arrInput = new float[ m_nImageSize * m_nImageSize * 3]; int[] intValues = new int[ m_nImageSize * m_nImageSize]; m_bitmapForNn.getPixels(intValues, 0, m_nImageSize, 0, 0, m_nImageSize, m_nImageSize); for (int i = 0; i < intValues.length; i++) { int val = intValues[i]; m_arrInput[i * 3 + 0] = ((val >> 16) & 0xFF) / 255f; m_arrInput[i * 3 + 1] = ((val >> 8) & 0xFF) / 255f; m_arrInput[i * 3 + 2] = (val & 0xFF) / 255f; } // --- tf = new TensorFlowInferenceInterface( getAssets(), MODEL_PATH); //Pass input into the tensorflow tf.feed(INPUT_NAME, m_arrInput, 1, m_nImageSize, m_nImageSize, 3); //compute predictions tf.run(new String[]{OUTPUT_NAME}, false); //copy output into PREDICTIONS array tf.fetch(OUTPUT_NAME, m_arrPrediction); } catch (Exception e) { e.getMessage(); } return null; } // --- @Override protected void onPostExecute(Void result) { super.onPostExecute(result); // --- enableControls(true); // --- tf = null; m_arrInput = null; # strResult contains 5 lines of text # with most probable dog breeds and # their probabilities m_strResult = ""; # What we do below is sorting the array # by probabilities (using map) # and getting in reverse order) the # first five entries TreeMap<Float, Integer> map = new TreeMap<Float, Integer>( Collections.reverseOrder()); for(int i = 0; i < m_arrPrediction.length; i++) map.put(m_arrPrediction[i], i); int i = 0; for (TreeMap.Entry<Float, Integer> pair : map.entrySet()) { float key = pair.getKey(); int idx = pair.getValue(); String strBreed = m_arrBreedsArray[idx]; m_strResult += strBreed + ": " + String.format("%.6f", key) + "\n"; i++; if (i > 5) break; } m_txtViewBreed.setVisibility(View.VISIBLE); m_txtViewBreed.setText(m_strResult); } } 


In onCreate() of the MainActivity, we need to add the onClickListener for the «Process» button:

 m_btn_process.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { processImage(); } }); 


processImage() , :

 private void processImage() { try { enableControls(false); // --- PredictionTask prediction_task = new PredictionTask(); prediction_task.execute(); } catch (Exception e) { e.printStackTrace(); } } 




UI- , . , .

instances , (flow of control): «» Facebook, , . , «» onCreate , onCreate .

:

1. onCreate MainActivity, onSharedIntent:

 protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); .... onSharedIntent(); .... 


onNewIntent:

 @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); onSharedIntent(); } 


onSharedIntent:
 private void onSharedIntent() { Intent receivedIntent = getIntent(); String receivedAction = receivedIntent.getAction(); String receivedType = receivedIntent.getType(); if (receivedAction.equals(Intent.ACTION_SEND)) { // If mime type is equal to image if (receivedType.startsWith("image/")) { m_txtViewBreed.setText(""); m_strResult = ""; Uri receivedUri = receivedIntent.getParcelableExtra( Intent.EXTRA_STREAM); if (receivedUri != null) { try { Bitmap bitmap = MediaStore.Images.Media.getBitmap( this.getContentResolver(), receivedUri); if(bitmap != null) { m_bitmap = bitmap; m_picView.setImageBitmap(m_bitmap); storeBitmap(); enableControls(true); } } catch (Exception e) { e.printStackTrace(); } } } } } 


onCreate ( ) onNewIntent ( ).




Bonne chance , , «» , «» .

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


All Articles