Determinación de la raza de un perro: un ciclo de desarrollo completo, desde una red neuronal en Python hasta una aplicación en Google Play

El progreso en el campo de las redes neuronales en general y el reconocimiento de patrones en particular ha llevado al hecho de que puede parecer que crear una aplicación de red neuronal para trabajar con imágenes es una tarea rutinaria. En cierto sentido, lo es: si se te ocurrió una idea relacionada con el reconocimiento de patrones, no dudes que alguien ya escribió algo así. Todo lo que se requiere de usted es encontrar el código correspondiente en Google y "compilarlo" del autor.

Sin embargo, todavía hay numerosos detalles que hacen que la tarea no sea tan irresoluble como ... aburrida, diría yo. Lleva demasiado tiempo, especialmente si eres un principiante que necesita liderazgo, paso a paso, un proyecto llevado a cabo ante tus ojos y completado de principio a fin. Sin lo habitual en tales casos, excusas de "saltear esta parte obvia".

En este artículo, consideraremos la tarea de crear un Identificador de raza de perro: crearemos y entrenaremos una red neuronal, y luego la trasladaremos a Java para Android y la publicaremos en Google Play.

Si desea ver el resultado final, aquí está: Aplicación NeuroDog en Google Play.

Sitio web con mi robótica (en progreso): robotics.snowcron.com .
Sitio web con el programa en sí, que incluye una guía: Guía del usuario de NeuroDog .

Y aquí hay una captura de pantalla del programa:

imagen



Declaración del problema.



Utilizaremos Keras: una biblioteca de Google para trabajar con redes neuronales. Esta es una biblioteca de alto nivel, lo que significa que es más fácil de usar en comparación con las alternativas que conozco. En todo caso, hay muchos libros de texto en Keras en la red, de alta calidad.

Utilizaremos CNN - Redes neuronales convolucionales. CNN (y configuraciones más avanzadas basadas en ellos) son el estándar de facto en reconocimiento de imágenes. Al mismo tiempo, entrenar una red de este tipo no siempre es fácil: debe elegir la estructura de red correcta, los parámetros de entrenamiento (todos estos índices de aprendizaje, impulso, L1 y L2, etc.). La tarea requiere importantes recursos informáticos y, por lo tanto, resolverla simplemente pasando por TODOS los parámetros fallará.

Esta es una de varias razones por las cuales en la mayoría de los casos usan el llamado "conocimiento de transferencia", en lugar del llamado enfoque "vainilla". Transfer Knowlege utiliza una red neuronal entrenada por alguien antes que nosotros (por ejemplo, Google) y generalmente para una tarea similar pero diferente. Tomamos las capas iniciales, reemplazamos las capas finales con nuestro propio clasificador, y funciona, y funciona muy bien.

Al principio, tal resultado puede ser sorprendente: ¿cómo es que tomamos una red de Google entrenada para distinguir gatos de sillas y reconoce razas de perros para nosotros? Para comprender cómo sucede esto, debe comprender los principios básicos del trabajo de las Redes neuronales profundas, incluidos los utilizados para el reconocimiento de patrones.

"Alimentamos" a la red una imagen (una matriz de números, es decir) como entrada. La primera capa analiza la imagen en busca de patrones simples, como "línea horizontal", "arco", etc. La siguiente capa recibe estos patrones como entrada, y produce patrones de segundo orden, como "pelaje", "esquina del ojo" ... En última instancia, obtenemos un rompecabezas desde el cual podemos reconstruir al perro: lana, dos ojos y una mano humana en los dientes.

Todo lo anterior se realizó con la ayuda de capas pre-entrenadas obtenidas por nosotros (por ejemplo, de Google). A continuación, agregamos nuestras capas y les enseñamos a extraer información de raza de estos patrones. Suena lógico

Para resumir, en este artículo crearemos CNN "vainilla" y varias variantes de "aprendizaje de transferencia" de diferentes tipos de redes. En cuanto a "vainilla": lo crearé, pero no planeo configurarlo seleccionando parámetros, ya que es mucho más fácil entrenar y configurar redes "pre-entrenadas".

Como planeamos enseñar a nuestra red neuronal a reconocer razas de perros, debemos "mostrarle" muestras de varias razas. Afortunadamente, hay un conjunto de fotografías creadas aquí para una tarea similar (el original está aquí ).

Entonces planeo portar la mejor de las redes recibidas para Android. Portar redes Kerasov a Android es relativamente simple, bien formalizado y haremos todos los pasos necesarios, por lo que no será difícil reproducir esta parte.

Luego publicaremos todo esto en Google Play. Naturalmente, Google resistirá, por lo que se utilizarán trucos adicionales. Por ejemplo, el tamaño de nuestra aplicación (debido a una red neuronal voluminosa) será mayor que el tamaño permitido del APK de Android aceptado por Google Play: tendremos que usar paquetes. Además, Google no mostrará nuestra aplicación en los resultados de búsqueda, esto se puede solucionar registrando etiquetas de búsqueda en la aplicación, o simplemente esperando ... una o dos semanas.

Como resultado, obtenemos una aplicación "comercial" completamente funcional (entre comillas, como se presenta de forma gratuita) para Android y que utiliza redes neuronales.

Entorno de desarrollo



Puede programar Keras de manera diferente, dependiendo del sistema operativo que utilice (se recomienda Ubuntu), la presencia o ausencia de una tarjeta de video, etc. No hay nada malo en el desarrollo en la computadora local (y, en consecuencia, su configuración), excepto que esta no es la forma más fácil.

Primero, instalar y configurar una gran cantidad de herramientas y bibliotecas lleva tiempo, y luego, cuando se lanzan nuevas versiones, tendrá que pasar tiempo nuevamente. En segundo lugar, las redes neuronales requieren una gran potencia informática para el entrenamiento. Puede acelerar (10 veces o más) este proceso si usa una GPU ... al momento de escribir este artículo, las GPU más adecuadas para este trabajo cuestan $ 2,000 - $ 7,000. Y sí, también deben configurarse.

Entonces iremos por el otro lado. El hecho es que Google permite que los erizos pobres como nosotros usen GPU de su clúster: de forma gratuita, para los cálculos relacionados con las redes neuronales, también proporciona un entorno totalmente configurado, todo junto, esto se llama Google Colab. El servicio le da acceso a Jupiter Notebook con python, Keras y una gran cantidad de otras bibliotecas ya configuradas. Todo lo que tiene que hacer es obtener una cuenta de Google (obtenga una cuenta de Gmail y esto le dará acceso a todo lo demás).

Por el momento, Colab puede ser contratado aquí , pero conociendo Google, esto puede cambiar en cualquier momento. Simplemente google Google Colab.

El problema obvio con el uso de Colab es que es un servicio WEB. ¿Cómo accedemos a nuestros datos? ¿Guardar la red neuronal después del entrenamiento, por ejemplo, descargar datos específicos de nuestra tarea, etc.?

Hay varias (al momento de escribir este artículo, tres) diferentes formas, utilizamos la que creo que es más conveniente: usamos Google Drive.

Google Drive es un almacenamiento de datos basado en la nube que funciona de manera muy similar a un disco duro normal, y puede mapearse en Google Colab (consulte el código a continuación). Después de eso, puede trabajar con él como lo haría con archivos en un disco local. Es decir, por ejemplo, para acceder a las fotos de perros para entrenar nuestra red neuronal, necesitamos subirlos a Google Drive, eso es todo.

Crear y entrenar una red neuronal



A continuación doy el código en Python, bloque por bloque (del cuaderno de Júpiter). Puede copiar este código en su Jupiter Notebook y ejecutarlo, bloque por bloque, también, ya que los bloques se pueden ejecutar de forma independiente (por supuesto, las variables definidas en el bloque inicial pueden ser necesarias en el último, pero esta es una dependencia obvia).

Inicialización



En primer lugar, montemos Google Drive. Solo dos líneas. Este código debe ejecutarse solo una vez en una sesión de Colab (por ejemplo, una vez cada 6 horas). Si lo llama por segunda vez mientras la sesión aún está "activa", se omitirá ya que la unidad ya está montada.

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


Al principio, se le pedirá que confirme sus intenciones, no hay nada complicado. Así es como se ve:

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


Una sección de inclusión completamente estándar; es posible que algunos de los archivos incluidos no sean necesarios, bueno ... lo siento. Además, dado que voy a probar diferentes redes neuronales, tendrá que comentar / descomentar algunos de los módulos incluidos para tipos específicos de redes neuronales: por ejemplo, para usar InceptionV3 NN, descomentar la inclusión de InceptionV3 y comentar, por ejemplo, ResNet50. O no: todo lo que cambia de esto es el tamaño de la memoria utilizada, y eso no es muy fuerte.

 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 


En Google Drive, creamos una carpeta para nuestros archivos. La segunda línea muestra su contenido:

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


Como puede ver, las fotos de los perros (copiadas del conjunto de datos de Stanford (ver arriba) en Google Drive) se guardan primero en la carpeta all_images . Más tarde, los copiaremos en los directorios de trenes, válidos y de prueba . Guardaremos modelos entrenados en la carpeta de modelos . En cuanto al archivo labels.csv, esto es parte del conjunto de datos con fotos, contiene una tabla de correspondencia de los nombres de imágenes y razas de perros.

Hay muchas pruebas que puede ejecutar para comprender qué obtuvimos exactamente para el uso temporal de Google. Por ejemplo:

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


Como puede ver, la GPU está realmente conectada, y si no, necesita encontrar y habilitar esta opción en la configuración de Jupiter Notebook.

A continuación, debemos declarar algunas constantes, como el tamaño de las imágenes, etc. Utilizaremos imágenes con un tamaño de 256x256 píxeles, esta es una imagen lo suficientemente grande como para no perder detalles, y lo suficientemente pequeña como para que todo encaje en la memoria. Sin embargo, tenga en cuenta que algunos tipos de redes neuronales que utilizaremos esperan imágenes de 224x224 píxeles. En tales casos, comentamos 256 y descomentamos 224.

Se aplicará el mismo enfoque (comentario uno: descomentar) a los nombres de los modelos que guardamos, simplemente porque no queremos sobrescribir archivos que aún pueden ser útiles.
 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" 


Carga de datos



En primer lugar, carguemos el archivo labels.csv y divídalo en las partes de capacitación y validación. Tenga en cuenta que todavía no hay una parte de prueba, ya que voy a hacer trampa para obtener más datos de entrenamiento.

 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 


Luego, copie los archivos de imagen a las carpetas de capacitación / validación / prueba, de acuerdo con los nombres de los archivos. La siguiente función copia los archivos cuyos nombres transferimos a la carpeta especificada.

 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) 


Como puede ver, solo copiamos un archivo para cada raza de perro como prueba . Además, al copiar, creamos subcarpetas, una para cada raza. En consecuencia, las fotografías se copian a las subcarpetas por raza.

Esto se hace porque Keras puede trabajar con un directorio de una estructura similar, cargando archivos de imagen según sea necesario, y no todos a la vez, lo que ahorra memoria. Cargar las 15,000 imágenes a la vez es una mala idea.

Tendremos que llamar a esta función solo una vez, ya que copia imágenes, y ya no es necesaria. En consecuencia, para uso futuro, debemos comentarlo:

 # 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) 


Obtenga una lista de razas de perros:

 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> 


Procesamiento de imagen



Vamos a utilizar la función de biblioteca de Keras llamada ImageDataGenerators. ImageDataGenerator puede procesar la imagen, escalar, rotar, etc. También puede aceptar una función de procesamiento que puede procesar imágenes adicionalmente.

 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] 


Presta atención al siguiente código:

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


Podemos normalizar (subdatos dentro del rango 0-1 en lugar del 0-255 original) en ImageDataGenerator. ¿Por qué entonces necesitamos un preprocesador? Como ejemplo, considere la llamada borrosa (comentada, no la uso): esta es la misma manipulación de imagen personalizada que puede ser arbitraria. Cualquier cosa, desde contraste hasta HDR.

Utilizaremos dos ImageDataGenerators diferentes, uno para capacitación y otro para validación. La diferencia es que para el entrenamiento necesitamos giros y escalas para aumentar la "variedad" de datos, pero para la validación, no necesitamos, al menos, no en esta tarea.

 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") 


Crear una red neuronal



Como ya se mencionó, vamos a crear varios tipos de redes neuronales. Cada vez llamaremos a otra función para crear, incluir otros archivos y, a veces, determinar un tamaño de imagen diferente. Entonces, para cambiar entre diferentes tipos de redes neuronales, debemos comentar / descomentar el código apropiado.

En primer lugar, cree una CNN "vainilla". No funciona bien, porque decidí no perder el tiempo depurándolo, pero al menos proporciona una base que se puede desarrollar si hay un deseo (por lo general, esta es una mala idea, ya que las redes pre-entrenadas dan el mejor resultado).

 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) 


Cuando creamos redes utilizando transferencia de aprendizaje , el procedimiento cambia:

 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 creación de otros tipos de redes sigue el mismo patrón:

 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) 


Advertencia: ganador! Este NN mostró el mejor resultado:

 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) 


Otro:

 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) 


Se pueden usar diferentes tipos de redes neuronales para diferentes tareas. Por lo tanto, además de los requisitos para la precisión de la predicción, el tamaño puede ser importante (el NN móvil es 5 veces más pequeño que Inception) y la velocidad (si necesitamos el procesamiento en tiempo real de una transmisión de video, entonces la precisión tendrá que ser sacrificada).

Entrenamiento de redes neuronales



En primer lugar, estamos experimentando , por lo que deberíamos poder eliminar las redes neuronales que hemos guardado, pero que ya no usamos. La siguiente función elimina NN si 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 forma en que creamos y eliminamos redes neuronales es bastante simple y directa. Primero, elimine. Cuando llame a borrar (solo), tenga en cuenta que el Jupiter Notebook tiene una función de "ejecutar selección", seleccione solo lo que desea usar y ejecútelo.

Luego creamos una red neuronal si su archivo no existía, o cargamos la llamada si existe: por supuesto, no podemos llamar a "eliminar" y luego esperamos que NN exista, así que para usar una red neuronal guardada, no llame a eliminar .

En otras palabras, podemos crear un nuevo NN, o usar el existente, dependiendo de la situación y de lo que estamos experimentando actualmente. Un escenario simple: entrenamos una red neuronal y luego nos fuimos de vacaciones. Regresaron y Google cerró la sesión, por lo que debemos cargar la guardada anteriormente: comentar "eliminar" y descomentar "cargar".

 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) 


Los puntos de control son un elemento muy importante de nuestro programa. Podemos crear una serie de funciones que deberían llamarse al final de cada era de entrenamiento y pasarlas al punto de control. Por ejemplo, puede guardar una red neuronal si muestra resultados mejores que los ya guardados.

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


Finalmente, enseñamos la red neuronal en el conjunto de entrenamiento:

 # 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 


Los gráficos de precisión y pérdida para la mejor de las configuraciones son los siguientes:




Como puede ver, la red neuronal está aprendiendo, y no está mal.

Prueba de red neuronal



Después de completar el entrenamiento, debemos probar el resultado; para esto, NN presenta imágenes que nunca había visto antes, las que copiamos en la carpeta de prueba, una para cada raza de perro.

 # --- 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 


Exportar una red neuronal a una aplicación Java



En primer lugar, necesitamos organizar la carga de la red neuronal desde el disco. La razón es clara: la exportación se lleva a cabo en otro bloque de código, por lo que lo más probable es que comencemos la exportación por separado, cuando la red neuronal esté en su estado óptimo. Es decir, inmediatamente antes de la exportación, en la misma ejecución del programa, no entrenaremos la red. Si usa el código que se muestra aquí, entonces no hay diferencia, se ha seleccionado la red óptima para usted. Pero si aprenderá algo propio, entonces entrenar todo de nuevo antes de ahorrar es una pérdida de tiempo, si antes de eso lo guardó todo.

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


Por la misma razón, para no saltar el código, incluyo los archivos necesarios para exportar aquí. Nadie te molesta en moverlos al comienzo del programa si tu sentido de la belleza lo requiere:

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


Una pequeña prueba después de cargar una red neuronal, solo para asegurarse de que todo esté cargado: funciona:

 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() 


imagen

Luego, necesitamos obtener los nombres de las capas de entrada y salida de la red (ya sea esta o la función de creación, debemos “nombrar” explícitamente las capas, lo que no hicimos).

 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 


Usaremos los nombres de las capas de entrada y salida más tarde cuando importamos la red neuronal en una aplicación Java.

Otro código de itinerancia en la red para obtener estos datos:

 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])) 


Pero no me gusta y no lo recomiendo.

El siguiente código exportará la red neuronal Keras a formato pb , el que capturaremos desde 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) 


Llamando a estas funciones para exportar una red neuronal:

 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") 


La última línea imprime la estructura de la red neuronal resultante.

Crear una aplicación de Android usando una red neuronal



La exportación de redes neuronales en Android está bien formalizada y no debería causar dificultades. Hay, como siempre, de varias maneras, las más populares (al momento de escribir).

En primer lugar, usamos Android Studio para crear un nuevo proyecto. Vamos a "cortar esquinas" porque nuestra tarea no es un tutorial de Android. Entonces la aplicación contendrá solo una actividad.

imagen

Como puede ver, agregamos la carpeta "activos" y copiamos nuestra red neuronal (la que exportamos anteriormente).

Archivo Gradle



En este archivo, debe realizar varios cambios. En primer lugar, necesitamos importar la biblioteca tensorflow-android . Se utiliza para trabajar con Tensorflow (y, en consecuencia, Keras) de Java:

imagen

otro obstáculo obvio: versionCode y versionName . Cuando la aplicación cambie, deberá cargar nuevas versiones en Google Play. Sin cambiar las versiones en gdadle (por ejemplo, 1 -> 2 -> 3 ...) no puede hacer esto, Google dará un error "esta versión ya existe".

Manifiesto



En primer lugar, nuestra aplicación será "pesada": la red neuronal de 100 Mb encajará fácilmente en la memoria de los teléfonos celulares modernos, pero abrir una instancia separada para cada foto "compartida" de Facebook es definitivamente una mala idea.

Por lo tanto, prohibimos crear más de una instancia de nuestra aplicación:

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


Al agregar android: launchMode = "singleTask" a MainActivity, le decimos a Android que abra (active) una copia existente de la aplicación, en lugar de crear otra instancia.

Luego, debemos incluir nuestra aplicación en la lista, que el sistema muestra cuando alguien "comparte" la imagen:

 <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> 


Finalmente, necesitamos solicitar características y permisos que utilizará nuestra aplicación:

 <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" /> 


Si está familiarizado con la programación para Android, esta parte no debería causar preguntas.

Aplicación de diseño.



Crearemos dos diseños, uno para retrato y otro para paisaje. Así es como se ve el diseño de retrato .

Lo que agregaremos: un campo grande (vista) para mostrar la imagen, una lista molesta de anuncios (que se muestra cuando se presiona el botón con un hueso), el botón Ayuda, botones para descargar una imagen de Archivo / Galería y capturar desde la cámara, y finalmente (inicialmente oculto) Botón "Proceso" para el procesamiento de imágenes.

imagen

La actividad en sí contiene toda la lógica de mostrar y ocultar, así como habilitar / deshabilitar botones, según el estado de la aplicación.

Mainactividad



Esta actividad hereda (extiende) la Actividad estándar de Android:

 public class MainActivity extends Activity 


Considere el código responsable del funcionamiento de la red neuronal.

En primer lugar, la red neuronal acepta Bitmap. Inicialmente, este es un mapa de bits grande (de tamaño arbitrario) de la cámara o de un archivo (m_bitmap), luego lo transformamos, lo que lleva a los 256x256 píxeles estándar (m_bitmapForNn). También almacenamos el tamaño del mapa de bits (256) en una constante:

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


Debemos decirle a la red neuronal los nombres de las capas de entrada y salida; los recibimos antes (ver listado), pero tenga en cuenta que en su caso pueden diferir:

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


Luego declaramos una variable para contener el objeto TensofFlow. Además, almacenamos la ruta al archivo de red neuronal (que se encuentra en los activos):

private TensorFlowInferenceInterface tf;
Cadena privada MODEL_PATH = 
	"archivo: ///android_asset/dogs.pb";


Almacenamos las razas de perros en la lista, para que luego se muestren al usuario, y no los índices de la matriz:
 private String[] m_arrBreedsArray; 


Inicialmente, descargamos Bitmap. Sin embargo, la red neuronal espera una serie de valores RGB, y su salida es una serie de probabilidades de que esta raza sea lo que se muestra en la imagen. En consecuencia, debemos agregar dos matrices más (tenga en cuenta que 120 es el número de razas de perros presentes en nuestros datos de entrenamiento):

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


Descargar la biblioteca de inferencia de tensorflow:

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


Dado que las operaciones de la red neuronal toman tiempo, necesitamos ejecutarlas en un hilo separado, de lo contrario existe la posibilidad de que recibamos un mensaje del sistema "la aplicación no responde", sin mencionar a un usuario insatisfecho.

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


En onCreate () de MainActivity, necesitamos agregar onClickListener para el botón "Proceso":

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


Aquí processImage () solo llama al hilo que describimos anteriormente:

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


Notas adicionales



No planeamos discutir los detalles de la programación de IU para Android, ya que esto ciertamente no se aplica a la tarea de portar redes neuronales. Sin embargo, una cosa aún vale la pena mencionar.

Cuando impedimos la creación de instancias adicionales de nuestra aplicación, también rompimos el orden normal de creación y eliminación de actividad (flujo de control): si "comparte" una imagen de Facebook y luego comparte otra, la aplicación no se reiniciará. Esto significa que la forma "tradicional" de capturar los datos transferidos en onCreate no será suficiente, ya que no se llamará a onCreate.

Aquí se explica cómo resolver este problema:

1. En onCreate en MainActivity, llame a la función onSharedIntent:

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


También agregamos un controlador para onNewIntent:

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


Aquí está la función onSharedIntent en sí:
 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(); } } } } } 


Ahora procesamos los datos transferidos en onCreate (si la aplicación no estaba en la memoria) o en onNewIntent (si se lanzó antes).




Buena suerteSi le gustó el artículo, por favor, haga clic en "Me gusta" en todas las formas posibles, también hay botones "sociales" en el sitio .

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


All Articles