Identificador da Raça Canina: Desenvolvimento do ciclo completo do programa Keras para o aplicativo Android. no mercado de jogo

Com o recente progresso das redes neurais em geral e o reconhecimento de imagens em particular, pode parecer que criar um aplicativo baseado em NN para reconhecimento de imagens seja uma operação simples de rotina. Bem, até certo ponto, é verdade: se você pode imaginar uma aplicação de reconhecimento de imagem, provavelmente alguém já fez algo semelhante. Tudo que você precisa fazer é pesquisar no Google e repetir.

No entanto, ainda existem inúmeros pequenos detalhes de que ... eles não são insolúveis, não. Eles simplesmente levam muito do seu tempo, especialmente se você é iniciante. O que seria de ajuda é um projeto passo a passo, feito bem na sua frente, do começo ao fim. Um projeto que não contém "esta parte é óbvia, então vamos ignorá-la". Bem, quase :)

Neste tutorial, examinaremos um identificador de raça de cachorro: criaremos e ensinaremos uma rede neural, depois a portaremos para Java para Android e publicaremos no Google Play.

Para aqueles que desejam ver um resultado final, aqui está o link para o NeuroDog App no Google Play.

Site com minha robótica: robotics.snowcron.com .
Site com: Guia do usuário do NeuroDog .

Aqui está uma captura de tela do programa:

imagem



Uma visão geral



Vamos usar a biblioteca do Keras: Google para trabalhar com redes neurais. É de alto nível, o que significa que a curva de aprendizado será acentuada, definitivamente mais rápida do que com outras bibliotecas que eu conheço. Familiarize-se com ele: existem muitos tutoriais online de alta qualidade.

Usaremos CNNs - Redes Neurais Convolucionais. CNNs (e redes mais avançadas baseadas nelas) são de fato padrão no reconhecimento de imagens. Entretanto, ensinar alguém adequadamente pode se tornar uma tarefa formidável: estrutura da rede, parâmetros de aprendizado (todas essas taxas de aprendizado, momentos, L1 e L2 e assim por diante) devem ser cuidadosamente ajustados e, como a tarefa exige muitos recursos computacionais, não pode simplesmente tentar todas as combinações possíveis.

Essa é uma das poucas razões pelas quais, na maioria dos casos, preferimos usar o "transferir conhecimento" para a chamada abordagem "baunilha". O Transfer Knowlege usa uma rede neural treinada por outra pessoa (pense no Google) para alguma outra tarefa. Em seguida, removemos as últimas camadas, adicionamos camadas próprias ... e isso faz milagres.

Pode parecer estranho: pegamos a rede do Google treinada para reconhecer gatos, flores e móveis e agora identifica raça de cães! Para entender como funciona, vamos dar uma olhada no funcionamento das redes neurais profundas, incluindo as usadas para reconhecimento de imagem.

Nós alimentamos uma imagem como entrada. A primeira camada de uma rede analisa a imagem em busca de padrões simples, como "linha horizontal curta", "um arco" e assim por diante. A próxima camada pega esses padrões (e onde eles estão localizados na imagem) e produz padrões de nível superior, como "pele", "canto do olho" etc. No final, temos um quebra-cabeça que pode ser combinado à descrição de um cachorro: pêlo, dois olhos, perna humana na boca e assim por diante.

Agora, tudo isso foi feito por um conjunto de camadas pré-treinadas que recebemos (do Google ou de outro grande jogador). Finalmente, adicionamos nossas próprias camadas e ensinamos a trabalhar com esses padrões para reconhecer raças de cães. Parece lógico.

Para resumir, neste tutorial, criaremos a CNN “vanilla” e algumas redes de “transfer learning” de diferentes tipos. Quanto ao "baunilha": vou usá-lo apenas como um exemplo de como isso pode ser feito, mas não vou ajustá-lo, pois as redes "pré-treinadas" são muito mais fáceis de usar. O Keras vem com poucas redes pré-treinadas, vou escolher algumas configurações e compará-las.

Como queremos que nossa Rede Neural reconheça raças de cães, precisamos "mostrar" amostras de imagens de diferentes raças. Felizmente, há um grande conjunto de dados criado para uma tarefa semelhante ( original aqui ). Neste artigo, vou usar a versão do Kaggle

Então eu vou portar o "vencedor" para o Android. Portar o Keras NN para o Android é relativamente fácil, e seguiremos todas as etapas necessárias.

Em seguida, publicaremos no Google Play. Como seria de esperar, o Google não cooperará, portanto, serão necessários poucos truques adicionais. Por exemplo, nossa rede neural excede o tamanho permitido do APK do Android: teremos que usar o pacote. Além disso, o Google não mostrará nosso aplicativo nos resultados de pesquisa, a menos que façamos certas coisas mágicas.

No final, teremos um aplicativo Android "comercial" totalmente funcional (entre aspas, pois é gratuito, embora esteja pronto para o mercado) com Android NN.

Ambiente de desenvolvimento



Existem algumas abordagens diferentes para a programação do Keras, dependendo do sistema operacional usado (o Ubuntu é recomendado), a placa de vídeo que você possui (ou não) e assim por diante. Não há nada de errado em configurar o ambiente de desenvolvimento no computador local e instalar todas as bibliotecas necessárias e assim por diante. Exceto ... existe uma maneira mais fácil.

Primeiro, a instalação e configuração de várias ferramentas de desenvolvimento leva tempo e você terá que gastar tempo novamente, quando novas versões estiverem disponíveis. Segundo, o treinamento de redes neurais requer muita energia computacional. Você pode acelerar o seu computador usando a GPU ... no momento em que este artigo foi escrito, uma GPU de topo para cálculos relacionados à NN custa entre 2000 e 7000 dólares. E a configuração também leva tempo.

Então, vamos usar uma abordagem diferente. Veja, o Google permite que as pessoas usem suas GPUs gratuitamente para cálculos relacionados a NN; ele também criou um ambiente totalmente configurado; todos juntos é chamado Google Colab. O serviço concede acesso a um Notebook Jupiter com Python, Keras e várias bibliotecas adicionais já instaladas. Tudo o que você precisa fazer é obter uma conta do Google (obter uma conta do Gmail e você terá acesso a tudo o mais) e é isso.

No momento da redação deste artigo, a Colab pode ser acessada por este link , mas pode mudar. Basta pesquisar no Google Colab.

Um problema óbvio com a Colab é que é um serviço WEB. Como você acessará SEUS arquivos a partir dele? Salvando redes neurais após a conclusão do treinamento, carregando dados específicos para sua tarefa e assim por diante?

Existem poucas (no momento em que escrevo - três) abordagens diferentes; vamos usar o que acredito ser o melhor: usar o Google Drive.

O Google Drive é um armazenamento em nuvem que funciona praticamente como um disco rígido e pode ser mapeado para o Google Colab (veja o código abaixo). Então você trabalha com ele como faria com um disco rígido local. Por exemplo, se você deseja acessar fotos de cães da Rede Neural criada em Colab, é necessário fazer o upload dessas fotos no Google Drive, só isso.

Criando e treinando o NN



Abaixo, vou percorrer o código Python, um bloco de código do Jupiter Notebook após o outro. Você pode copiar esse código para o seu notebook e executá-lo, pois os blocos podem ser executados independentemente um do outro.

Inicialização



Primeiro de tudo, vamos montar o Google Drive. Apenas duas linhas de código. Esse código precisa ser executado apenas uma vez por sessão da Colab (digamos, uma vez a cada seis horas de trabalho). Se você executá-lo pela segunda vez, ele será ignorado, pois a unidade já está montada.

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


A primeira vez que você será solicitado a confirmar a montagem - nada complicado aqui. É assim:

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


Um padrão bastante inclui seção; provavelmente algumas das inclusões não são necessárias. Além disso, como vou testar diferentes configurações de NN, você terá que comentar / descomentar algumas delas para um tipo específico de NN: por exemplo, para usar o InceptionV3 tipo NN, descomentar InceptionV3 e comentar, por exemplo, ResNet50. Ou não: você pode manter esses itens descomentados, ele usará mais memória, mas é tudo.

 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 


No Google Drive, vamos criar uma pasta para nossos arquivos. A segunda linha exibe seu conteúdo:

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


Como você pode ver, fotos de cães (aquelas copiadas do conjunto de dados de Stanford (veja acima) para o Google Drive, são armazenadas inicialmente na pasta all_images . Mais tarde, vamos copiá-las para treinar, validar e testar pastas. Vamos salvar modelos treinados na pasta models . Quanto ao arquivo labels.csv, ele faz parte de um conjunto de dados, mapeia os arquivos de imagem para raças de cães.

Existem muitos testes que você pode executar para descobrir o que você tem; vamos executar apenas um:

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


Ok, a GPU está conectada. Caso contrário, encontre-o nas configurações do Jupiter Notebook e ligue-o.

Agora precisamos declarar algumas constantes que vamos usar, como o tamanho de uma imagem que a Rede Neural deve esperar e assim por diante. Observe que usamos uma imagem de 256x256, pois ela é grande o suficiente de um lado e cabe na memória do outro. No entanto, alguns tipos de redes neurais que estamos prestes a usar esperam uma imagem de 224x224. Para lidar com isso, quando necessário, comente o tamanho da imagem antiga e remova o comentário de uma nova.

A mesma abordagem (comentar um - descomentar o outro) se aplica aos nomes dos modelos que salvamos, simplesmente porque não queremos sobrescrever o resultado de um teste anterior quando tentamos uma nova configuração.
 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" 


Carregando dados



Primeiro, vamos carregar o arquivo labels.csv e dividir seu conteúdo em partes de treinamento e validação. Observe que ainda não há uma parte de teste, como vou trapacear um pouco, a fim de obter mais dados para o treinamento.

 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 


A seguir, precisamos copiar os arquivos de imagem reais para pastas de treinamento / validação / teste, de acordo com a matriz de nomes de arquivos que passamos. A função a seguir copia arquivos com nomes fornecidos para uma pasta 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 você pode ver, copiamos apenas um arquivo para cada raça de cães em uma pasta de teste . À medida que copiamos os arquivos, também criamos subpastas - uma subpasta por cada raça de cães. As imagens de cada raça em particular são copiadas em sua subpasta.

O motivo é que o Keras pode trabalhar com uma estrutura de diretórios organizada dessa maneira, carregando os arquivos de imagem conforme necessário, economizando memória. Seria uma péssima idéia carregar todas as 15.000 imagens na memória de uma só vez.

Chamar essa função toda vez que executamos nosso código seria um exagero: as imagens já foram copiadas, por que devemos copiá-las novamente. Portanto, comente após o primeiro uso:

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


Além disso, precisamos da lista de raças de cães:

 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> 


Processando imagens



Vamos usar o recurso do Keras chamado ImageDataGenerators. ImageDataGenerator pode processar uma imagem, redimensioná-la, girar e assim por diante. Ele também pode ter uma função de processamento que faz manipulações de imagem personalizadas.

 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] 


Observe a seguinte linha:

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


Podemos realizar a normalização (ajustando o intervalo de 0-255 do canal de imagem a 0-1) no próprio ImageDataGenerator. Então, por que precisaríamos de pré-processador? Como exemplo, forneci a função de desfoque (comentada): essa é uma manipulação de imagem personalizada. Você pode usar qualquer coisa, desde nitidez a HDR aqui.

Vamos usar dois ImageDataGenerators diferentes, um para treinamento e outro para validação. A diferença é que precisamos de rotações e zoom para treinamento, para tornar as imagens mais "diversas", mas não precisamos delas para validação (não nesta tarefa).

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


Criando rede neural



Como foi mencionado acima, vamos criar alguns tipos de redes neurais. Cada vez que usamos uma função diferente, diferentes incluem biblioteca e, em alguns casos, diferentes tamanhos de imagem. Portanto, para alternar de um tipo de rede neural para outro, é necessário comentar / descomentar o código correspondente.

Primeiro, vamos criar a CNN "vanilla". O desempenho é fraco, pois não o otimizei, mas pelo menos fornece uma estrutura que você pode usar para criar sua própria rede (geralmente, é uma má idéia, pois existem redes pré-treinadas disponíveis).

 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) 


Quando criamos a Rede Neural usando o aprendizado por transferência , o procedimento muda:

 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) 


A criação de outros tipos de NNs pré-treinados é muito semelhante:

 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) 


Atenção: o vencedor! Este NN demonstrou os melhores resultados:

 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) 


Mais um:

 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) 


Diferentes tipos de NNs são usados ​​em diferentes situações. Além dos problemas de precisão, os dimensionadores (o NN móvel é 5 vezes menor que o do início) e a velocidade (se precisarmos de análise em tempo real de um fluxo de vídeo, talvez seja necessário sacrificar a precisão).

Treinando a rede neural



Antes de tudo, estamos experimentando , portanto, precisamos poder excluir as NNs salvas anteriormente, mas que não precisam mais. A seguinte função exclui NN se o arquivo existir:

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


A maneira como criamos e excluímos NNs é simples. Primeiro, excluímos. Agora, se você não quiser chamar delete , lembre-se de que o Jupiter Notebook tem uma função de "seleção de execução" - selecione apenas o que você precisa e execute-o.

Em seguida, criamos o NN se o arquivo não existir ou o carregamos se o arquivo existir: é claro, não podemos chamar "delete" e esperar que o NN exista; portanto, para usar a rede salva anteriormente, não chame delete .

Em outras palavras, podemos criar um novo NN ou usar um já existente, dependendo do que estamos experimentando no momento. Um cenário simples: treinamos o NN e partimos para férias. O Google efetuou o logout, portanto, precisamos recarregar o NN: comente a parte "delete" e remova o comentário da parte "load".

 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) 


Os pontos de verificação são muito importantes ao ensinar as NNs. Você pode criar uma matriz de funções a serem chamadas no final de cada época de treinamento; por exemplo, você pode salvar o NN se apresentar resultados melhores que o último salvo.

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


Por fim, ensinaremos nosso NN usando o conjunto de treinamento:

 # 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 


Aqui estão os gráficos de precisão e perda para o vencedor NN:




Como você pode ver, a rede aprende bem.

Testando a rede neural



Após a fase de treinamento estar concluída, precisamos realizar testes; Para isso, o NN recebe imagens que nunca viu. Lembre-se, deixamos de lado uma imagem para cada espécie de cão.

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


Exportando NN para Java



Primeiro, precisamos carregar o NN. O motivo é que exportar é um bloco de código separado; portanto, é provável que o executemos separadamente, sem treinar novamente o NN. Ao usar meu código, você realmente não se importa, mas se você desenvolvesse seu próprio desenvolvimento, tentaria evitar treinar novamente a mesma rede uma vez após a outra.

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


Pelo mesmo motivo - este é um bloco de código separado - estamos usando inclusões adicionais aqui. Nada nos impede de subir, é claro:

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


Um pequeno teste, apenas para garantir que carregamos tudo certo:

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


imagem

A seguir, precisamos obter nomes das camadas de entrada e saída da nossa rede (a menos que tenhamos usado o parâmetro "name" ao criar a rede, o que não fizemos).

 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 


Vamos usar os nomes da camada de entrada e saída posteriormente, ao importar o NN no aplicativo Java Android.

Também podemos usar o seguinte código para obter essas informaçõ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])) 


No entanto, a primeira abordagem é preferida.

A função a seguir exporta o Keras Neural Network para o formato pb , que usaremos no 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) 


Vamos usar essas funções para criar um NN exportes:

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


A última linha imprime a estrutura do nosso NN.

Criando um aplicativo Android com NN



Exportando NN para o aplicativo Android. está bem formalizado e não deve apresentar dificuldades. Há, como sempre, mais de uma maneira de fazê-lo; vamos usar os mais populares (pelo menos no momento).

Primeiro de tudo, use o Android Studio para criar um novo projeto. Vamos cortar um pouco os cantos, para que ele contenha apenas uma única atividade.

imagem

Como você pode ver, adicionamos a pasta "assets" e copiamos nosso arquivo de rede neural lá.

Arquivo Gradle



Há algumas mudanças que precisamos fazer para classificar os arquivos. Primeiro, precisamos importar a biblioteca tensorflow-android . É usado para manipular o Tensorflow (e Keras, de acordo) do Java:

imagem

Como um detalhe "difícil de encontrar" adicional, observe as versões: versionCode e versionName . Enquanto trabalha no seu aplicativo, você precisa fazer o upload de novas versões para o Google Play. Sem atualizar versões (algo como 1 -> 2 -> 3 ...), você não poderá fazer isso.

Manifesto



Primeiro de tudo, nosso aplicativo. vai ser "pesado" - uma rede neural de 100 Mb cabe facilmente na memória dos telefones modernos, mas abrir uma instância separada sempre que o usuário "compartilha" uma imagem do Facebook definitivamente não é uma boa idéia.

Portanto, garantiremos que haja apenas uma instância do nosso aplicativo:

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


Ao adicionar android: launchMode = "singleTask" a MainActivity, pedimos ao Android para abrir um aplicativo existente, em vez de iniciar outra instância.

Então nos certificamos de nosso aplicativo. aparece em uma lista de aplicativos capazes de lidar com imagens compartilhadas :

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


Por fim, precisamos solicitar recursos e permissões, para que o aplicativo possa acessar a funcionalidade do sistema necessária:

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


Se você está familiarizado com a programação do Android, esta parte não deve gerar dúvidas.

Layout do aplicativo.



Vamos criar dois layouts, um para o modo retrato e outro para o modo paisagem. Aqui está o layout Retrato .

O que temos aqui: uma visão ampla para mostrar uma imagem, uma lista bastante irritante de comerciais (mostrada quando o botão "bone" é pressionado), botões "Help", botões para carregar uma imagem do File / Gallery e da Camera e finalmente, um botão (processo inicialmente oculto) "Process".

imagem

Na própria atividade, implementaremos alguma lógica que mostra / oculta e ativa / desativa os botões, dependendo do estado do aplicativo.

Atividade principal



A atividade estende uma atividade padrão do Android:

 public class MainActivity extends Activity 


Vamos dar uma olhada no código responsável pelas operações NN.

Primeiro de tudo, o NN aceita um bitmap. Originalmente, é um bitmap grande do arquivo ou da câmera (m_bitmap), depois o transformamos em um bitmap padrão de 256x256 (m_bitmapForNn). Também mantemos as dimensões da imagem (256) em uma constante:

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


Precisamos dizer ao NN quais são os nomes para as camadas de entrada e saída; se você consultar a lista acima, verá que os nomes são (no nosso caso! o seu caso pode ser diferente!):

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


Em seguida, declaramos a variável para conter o objeto TensofFlow. Além disso, armazenamos o caminho para o arquivo NN nos ativos:

 private TensorFlowInferenceInterface tf;
 string privada MODEL_PATH = 
	 "arquivo: ///android_asset/dogs.pb";


Raças de cães, para apresentar ao usuário uma informação significativa, em vez de índices na matriz:
 private String[] m_arrBreedsArray; 


Inicialmente, carregamos um bitmap. No entanto, o próprio NN espera uma matriz de valores RGB, e sua saída é uma matriz de probabilidades da imagem apresentada ser uma raça específica. Portanto, precisamos adicionar mais duas matrizes (observe que 120 é o número de raças em nosso conjunto de dados de treinamento):

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


Carregue a biblioteca de inferência tensorflow

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


Como a operação da NN é longa, precisamos executá-la em um thread separado, caso contrário, há uma boa chance de acessar o aplicativo "system". aviso de não responder ", para não mencionar estragar a experiência do usuário.

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


No onCreate () do MainActivity, precisamos adicionar o onClickListener para o botão "Process":

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


O que processImage () faz é simplesmente chamar o thread que vimos acima:

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


Detalhes Adicionais



Não vamos discutir o código relacionado à interface do usuário neste tutorial, pois é trivial e definitivamente não faz parte da tarefa "portando NN". No entanto, há poucas coisas que devem ser esclarecidas.

Quando prevemos nosso aplicativo. ao iniciar várias instâncias, evitamos, ao mesmo tempo, um fluxo normal de controle: se você compartilha uma imagem do Facebook e depois outra, o aplicativo não será reiniciado. Isso significa que a maneira "tradicional" de manipular dados compartilhados, capturando-os no onCreate, não é suficiente no nosso caso, pois o onCreate não é chamado em um cenário que acabamos de criar.

Aqui está uma maneira de lidar com a situação:

1. No onCreate de MainActivity, chame a função onSharedIntent:

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


Além disso, adicione um manipulador para onNewIntent:

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


A própria função 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(); } } } } } 


Agora, lidamos com a imagem compartilhada do onCreate (se o aplicativo acabou de ser iniciado) ou do onNewIntent se uma instância foi encontrada na memória.




Boa sorte Se você gosta deste artigo, "gostei" dele nas redes sociais, também existem botões sociais no próprio site .

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


All Articles