随着神经网络(特别是图像识别)领域的最新进展,似乎创建基于NN的图像识别应用程序是一项简单的常规操作。 好吧,在某种程度上说是正确的:如果您可以想象图像识别的应用,那么很可能有人已经做了类似的事情。 您需要做的就是将它添加到Google并重复进行。
但是,仍然有无数小细节……它们不是无法解决的,不是。 它们只会占用您太多时间,尤其是对于初学者而言。 这将是一个逐步的项目,这将对您有所帮助,然后从头开始。 一个不包含“这部分很明显,所以我们跳过它”语句的项目。 好吧,几乎:)
在本教程中,我们将逐步介绍一个“狗品种标识符”:我们将创建并教授一个神经网络,然后将其移植到Java for Android并在Google Play上发布。
对于那些想要查看最终结果的人,这里是Google Play上
NeuroDog App的链接。
使用我的机器人
技术的网站:
robotics.snowcron.com 。
网站:
NeuroDog用户指南 。
这是该程序的屏幕截图:

概述
我们将使用Keras:Google的与神经网络合作的库。 它是高级的,这意味着学习曲线将是陡峭的,绝对比我所知道的其他库快。 使自己熟悉它:在线上有许多高质量的教程。
我们将使用CNN-卷积神经网络。 CNN(以及基于它们的更高级的网络)在图像识别中已成为事实上的标准。 但是,正确地教人可能会成为一项艰巨的任务:网络的结构,学习参数(所有这些学习率,动量,L1和L2等)都应谨慎调整,并且由于该任务需要大量的计算资源,因此我们不能简单地尝试所有可能的组合。
这是为什么在大多数情况下我们更喜欢使用“转移知识”而不是所谓的“香草”方法的少数原因之一。 Transfer Knowlege使用由其他人(例如Google)训练的神经网络来执行其他一些任务。 然后,我们删除它的最后几层,添加我们自己的层……这行之有效。
听起来很奇怪:我们让Google的网络接受了识别猫,花和家具的培训,现在它可以识别狗的品种! 为了了解其工作原理,让我们看一下深度神经网络(包括用于图像识别的神经网络)的工作方式。
我们将图像作为输入。 网络的第一层分析图像中的简单图案,例如“短水平线”,“拱形”等。 下一层采用这些图案(以及它们在图像上的位置),并产生更高级别的图案,例如“毛发”,“眼角”等。 最后,我们有一个谜题,可以组合成对狗的描述:毛皮,两只眼睛,嘴里的人的腿等等。
现在,这一切都是由我们(从Google或其他一些大型公司)获得的一组经过预训练的图层完成的。 最后,我们在其上添加自己的图层,并教会其使用这些模式来识别狗的品种。 听起来合乎逻辑。
总而言之,在本教程中,我们将创建“香草” CNN和几个不同类型的“转移学习”网络。 至于“香草”:我将仅以它为例进行说明,但由于“预训练”网络更易于使用,因此我不会对其进行微调。 Keras带有一些预先训练的网络,我将选择几种配置并进行比较。
因为我们希望我们的神经网络能够识别狗的品种,所以我们需要“显示”它的不同品种的样本图像。 幸运的是,为类似的任务创建了一个
大型数据集 (
此处为
原始 )。 在本文中,我将使用
Kaggle的
版本然后,我要将“优胜者”移植到Android。 将Keras NN移植到Android相对容易,我们将逐步完成所有必需的步骤。
然后,我们将其发布在Google Play上。 正如人们所期望的那样,Google不会合作,因此几乎不需要其他技巧。 例如,我们的神经网络超出了Android APK的允许大小:我们将不得不使用bundle。 此外,除非我们做某些不可思议的事情,否则Google不会在搜索结果中显示我们的应用程序。
最后,我们将提供一个功能全面的“商业”(报价,因为它是免费的,尽管已经投入市场)是Android NN支持的应用程序。
开发环境
Keras编程的方法很少,这取决于所使用的操作系统(建议使用Ubuntu),所拥有(或没有)视频卡等。 在本地计算机上配置开发环境并安装所有必需的库等没有错。 除了...还有一种更简单的方法。
首先,安装和配置多个开发工具会花费一些时间,并且当新版本可用时,您将不得不再次花费时间。 其次,训练神经网络需要大量的计算能力。 您可以通过使用GPU来加速计算机的工作……在撰写本文时,用于NN相关计算的顶级GPU的价格为2000-7000美元。 配置它也需要时间。
因此,我们将使用另一种方法。 可以看到,Google允许人们免费使用其GPU进行与NN相关的计算,它还创建了一个完全配置的环境; 统称为Google Colab。 该服务使您可以访问带有Python,Keras和大量已安装的其他库的Jupiter Notebook。 您所需要做的就是获得一个Google帐户(获得一个Gmail帐户,您将可以访问其他所有内容),仅此而已。
在撰写本文时,可以
通过此链接访问Colab,但是它可以更改。 只需用谷歌搜索“ Google Colab”。
Colab的一个明显问题是它是一个Web服务。 您将如何从中访问您的文件? 培训结束后保存神经网络,加载特定于您任务的数据等吗?
几乎没有(在撰写本文之时-三种)不同的方法。 我们将使用我认为最好的方法:使用Google云端硬盘。
Google云端硬盘是一种云存储,可以像硬盘一样工作,并且可以映射到Google Colab(请参见下面的代码)。 然后,您就可以像处理本地硬盘一样使用它。 因此,例如,如果您想从在Colab中创建的神经网络访问狗的照片,则必须将这些照片上传到Google云端硬盘。
创建和训练NN
下面,我将逐步介绍Python代码,这是Jupiter Notebook中的另一段代码。 您可以将代码复制到笔记本中并运行它,因为可以相互独立地执行块。
初始化
首先,让我们安装Google云端硬盘。 只需两行代码。 该代码在每个Colab会话中仅需要执行一次(例如,每六个小时的工作一次)。 如果您第二次运行它,则由于已安装驱动器,因此它将被跳过。
from google.colab import drive drive.mount('/content/drive/')
首次要求您确认安装-这里没有什么复杂的。 看起来像这样:
>>> Go to this URL in a browser: ... >>> Enter your authorization code: >>> ·········· >>> Mounted at /content/drive/
一个相当标准的
include部分; 最有可能不需要其中的一些。 另外,当我要测试不同的NN配置时,您将必须针对特定类型的NN注释/取消注释其中的一些配置:例如,使用InceptionV3类型的NN,取消注释InceptionV3并注释(例如,ResNet50)。 还是不行:您可以不注释那些包含项,它将使用更多的内存,仅此而已。
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
在Google云端硬盘上,我们将为文件创建一个文件夹。 第二行显示其内容:
working_path = "/content/drive/My Drive/DeepDogBreed/data/" !ls "/content/drive/My Drive/DeepDogBreed/data" >>> all_images labels.csv models test train valid
如您所见,狗的照片(从斯坦福数据集(见上文)复制到Google云端硬盘的照片)最初存储在
all_images文件夹中。稍后,我们将它们复制到
训练,有效和
测试文件夹中。我们将保存在
models文件夹中训练有素的模型,对于labels.csv文件,它是数据集的一部分,它将图像文件映射到犬种。
您可以运行许多测试来弄清楚您拥有什么,让我们仅运行一个:
好的,GPU已连接。 如果没有,请在Jupiter Notebook设置中找到它并将其打开。
现在,我们需要声明一些将要使用的常量,例如神经网络应该期望的图像大小等等。 请注意,我们使用256x256的图片,因为该图片的一侧足够大,而另一侧可以容纳在内存中。 但是,我们将要使用的某些类型的神经网络期望224x224的图像。 要处理此问题,必要时,注释旧的图像大小,然后取消注释新的图像大小。
相同的方法(一种注释-另一种注释)适用于我们保存的模型名称,这仅仅是因为我们在尝试新的配置时不想覆盖先前测试的结果。
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
载入资料
首先,让我们加载
labels.csv文件,并将其内容拆分为训练和验证部分。 请注意,目前还没有测试部分,我将作弊,以获取更多训练数据。
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
接下来,我们需要根据传递的文件名数组将实际的图像文件复制到training / validation / testing文件夹中。 以下功能将具有提供名称的文件复制到指定的文件夹。
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] + "/")
如您所见,我们仅将每种犬只的一个文件复制到一个
测试文件夹中。 复制文件时,我们还会创建子文件夹-每个品种的狗一个子文件夹。 每个特定品种的图像都会复制到其子文件夹中。
原因是Keras可以使用以这种方式组织的目录结构,根据需要加载图像文件,从而节省了内存。 同时将所有15,000张图像加载到内存中是一个非常糟糕的主意。
每次我们运行代码时都调用此函数将是一个过大的杀手:图像已经被复制,为什么我们要再次复制它们。 因此,请先删除注释后再使用:
此外,我们需要狗的品种清单:
breeds = np.unique(labels['breed']) map_characters = {}
处理图像
我们将使用Keras的功能ImageDataGenerators。 ImageDataGenerator可以处理图像,调整图像大小,旋转等。 它还可以采用执行自定义图像处理的
处理功能。
def preprocess(img): img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE), interpolation = cv2.INTER_AREA)
请注意以下行:
我们可以在ImageDataGenerator本身中执行归一化(将图像通道的0-255范围调整为0-1)。 那么为什么我们需要预处理器? 例如,我提供了(注释掉)
模糊功能:这是一种自定义图像处理。 您可以在此处使用从锐化到HDR的任何内容。
我们将使用两种不同的ImageDataGenerator,一种用于训练,另一种用于验证。 不同之处在于,我们需要旋转和缩放来进行训练,以使图像更加“多样化”,但我们不需要它来进行验证(不在此任务中)。
train_datagen = ImageDataGenerator( preprocessing_function=preprocess,
创建神经网络
如上所述,我们将创建几种类型的神经网络。 每次我们使用不同的功能时,都会使用不同的包含库,在某些情况下还会使用不同的图像尺寸。 因此,要从一种类型的神经网络切换到另一种类型,您需要注释/取消注释相应的代码。
首先,让我们创建“香草” CNN。 由于我尚未对其进行优化,因此它的性能很差,但至少它提供了可用于创建自己的网络的框架(通常,这是一个坏主意,因为有可用的预先训练的网络)。
def createModelVanilla(): model = Sequential()
当我们使用
转移学习创建神经网络时,过程将更改:
def createModelMobileNetV2():
创建其他类型的预训练NN非常相似:
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)
Attn:获胜者! 该NN展示了最佳结果:
def createModelInceptionV3():
一:
def createModelNASNetMobile():
在不同情况下使用不同类型的NN。 除了精度问题外,大小问题(移动NN比Inception规模小5倍)和速度(如果我们需要对视频流进行实时分析,则可能不得不牺牲精度)。
训练神经网络
首先,我们正在
试验 ,因此我们需要能够删除之前保存的NN,但不再需要了。 如果文件存在,以下函数将删除NN:
我们创建和删除NN的方法很简单。 首先,我们删除。 现在,如果您不希望调用
delete ,只需记住Jupiter Notebook具有“运行选择”功能-仅选择所需的内容,然后运行它。
然后,如果文件不存在,则创建NN;如果文件存在,则将其
加载 :当然,我们不能调用“ delete”,然后期望NN存在,因此要使用以前保存的网络,请不要调用
delete 。
换句话说,我们可以创建新的NN或使用现有的NN,具体取决于我们目前正在尝试的内容。 一个简单的场景:我们已经训练了神经网络,然后去度假。 Google已将我们注销,因此我们需要重新加载NN:注释掉“删除”部分,然后取消注释“加载”部分。
deleteSavedNet(working_path + strModelFileName)
在教授神经网络时,
检查点非常重要。 您可以创建一个函数数组,在每个训练时期的末尾调用该函数,例如,如果显示的结果比上一个保存的结果更好,则可以保存NN。
checkpoint = ModelCheckpoint(working_path + strModelFileName, monitor='val_acc', verbose=1, save_best_only=True, mode='auto', save_weights_only=False) callbacks_list = [ checkpoint ]
最后,我们将使用训练集教授NN:
以下是获胜者NN的准确性和损失图:


如您所见,网络学习得很好。
测试神经网络
培训阶段完成后,我们需要进行测试; 为此,向NN展示了它从未见过的图像。 您可能还记得,我们为每种狗留了一张图片。
将NN导出到Java
首先,我们需要加载NN。 原因是,导出是一个单独的代码块,因此我们很可能单独运行它,而无需重新训练NN。 使用我的代码时,您并不在乎,但是如果您进行自己的开发,则应避免一次又一次地重新训练
同一个网络。
出于相同的原因-这是以某种方式单独的代码块-我们在这里使用其他包含。 当然,没有什么可以阻止我们向上移动它们:
from keras.models import Model from keras.models import load_model from keras.layers import * import os import sys import tensorflow as tf
进行一些测试,以确保我们正确加载了所有内容:
img = image.load_img(working_path + "test/affenpinscher.jpg")

接下来,我们需要获取网络输入和输出层的名称(除非在创建网络时使用了“ name”参数,否则就没有)。
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
稍后,当在Android Java应用程序中导入NN时,我们将使用输入和输出层的名称。
我们还可以使用以下代码获取此信息:
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()
但是,首选方法。
以下函数将Keras神经网络导出为
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)
让我们使用这些函数来创建导出NN:
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")
最后一行显示了我们的神经网络的结构。
创建具有NN功能的Android应用
将NN导出到Android应用。 已经正规化,不应造成任何困难。 和往常一样,有多种方法可以做到这一点。 我们将使用最受欢迎(至少目前)。
首先,使用Android Studio创建一个新项目。 我们将偷工减料,因此它仅包含一个活动。

如您所见,我们添加了“ assets”文件夹,并在此处复制了神经网络文件。
摇篮文件
我们需要对gradle文件进行一些更改。 首先,我们必须导入
tensorflow-android库。 它用于从Java处理Tensorflow(以及相应的Keras):

作为其他“很难找到”的细节,请注意版本:
versionCode和
versionName 。 在处理应用程序时,您需要将新版本上传到Google Play。 如果不更新版本(例如1-> 2-> 3 ...),您将无法执行更新。
清单
首先,我们的应用程序。 将会变得“沉重”-一个100 Mb的神经网络很容易容纳在现代手机的内存中,但是每次用户从Facebook“共享”图像时都打开一个单独的实例绝对不是一个好主意。
因此,我们将确保应用程序只有一个实例:
<activity android:name=".MainActivity" android:launchMode="singleTask">
通过向MainActivity添加
android:launchMode =“ singleTask” ,我们告诉Android打开现有应用程序,而不是启动另一个实例。
然后,确保我们的应用程序。 出现在能够处理
共享图像的应用程序列表中:
<intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <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编程,则本部分不会提出任何问题。
应用的布局。
我们将创建两种布局,一种用于纵向,另一种用于横向模式。 这是
纵向布局 。
我们在这里拥有的功能:显示图像的大视图,相当烦人的广告列表(按下“骨头”按钮时显示),“帮助”按钮,用于从文件/库和相机中加载图像的按钮,以及最后,一个(最初隐藏的)按钮“ Process”。

在活动本身中,我们将根据应用程序的状态来实现一些显示/隐藏以及启用/禁用按钮的逻辑。
主要活动
该活动扩展了标准的Android活动:
public class MainActivity extends Activity
让我们看一下负责NN操作的代码。
首先,NN接受位图。 最初,它是来自文件或相机的大位图(m_bitmap),然后将其转换为标准的256x256位图(m_bitmapForNn)。 我们还将图像尺寸(256)保持恒定:
static Bitmap m_bitmap = null; static Bitmap m_bitmapForNn = null; private int m_nImageSize = 256;
我们需要告诉NN输入和输出层的名称是什么; 如果您查阅上面的清单,您会发现名称是(在我们的情况下!您的情况可能有所不同!):
private String INPUT_NAME = "input_7_1"; private String OUTPUT_NAME = "output_1";
然后,我们声明该变量以保存TensofFlow对象。 另外,我们在资产中存储NN文件的路径:
私有TensorFlowInferenceInterface tf;
私有字符串MODEL_PATH =
“文件:////android_asset/dogs.pb”;
品种的狗,向用户展示有意义的信息,而不是数组中的索引:
private String[] m_arrBreedsArray;
最初,我们加载位图。 但是,NN本身需要一个RGB值数组,并且它的输出是所呈现图像是特定品种的概率数组。 因此,我们需要再添加两个数组(请注意,训练数据集中的品种数为120):
private float[] m_arrPrediction = new float[120]; private float[] m_arrInput = null;
加载tensorflow推理库
static { System.loadLibrary("tensorflow_inference"); }
由于NN的操作很长,我们需要在单独的线程中执行它,否则很有可能会碰到系统“ app”。 “不响应”警告,更不用说破坏用户体验了。
class PredictionTask extends AsyncTask<Void, Void, Void> { @Override protected void onPreExecute() { super.onPreExecute(); }
在MainActivity的onCreate()中,我们需要为“ Process”按钮添加onClickListener: m_btn_process.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { processImage(); } });
processImage()所做的只是简单地调用我们在上面看到的线程: private void processImage() { try { enableControls(false);
其他细节
We are not going to discuss UI-related code in this tutorial, as it is trivial and definitely is not part of «porting NN» task. However, there are few thing that should be clarified.
When we prevemted our app. from launching multiple instances, we have prevented, in the same time, a normal flow on control: if you share an image from Facebook, and then share another one, the application will not be restarted. It means that the «traditional» way of handling shared data by catching it in onCreate is not sufficient in our case, as onCreate is not called in a scenario we just created.
Here is a way to handle the situation:
1. In onCreate of MainActivity, call the onSharedIntent function:
protected void onCreate( Bundle savedInstanceState) { super.onCreate(savedInstanceState); .... onSharedIntent(); ....
Also, add a handler for onNewIntent:
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); onSharedIntent(); }
The onSharedIntent function itself:
private void onSharedIntent() { Intent receivedIntent = getIntent(); String receivedAction = receivedIntent.getAction(); String receivedType = receivedIntent.getType(); if (receivedAction.equals(Intent.ACTION_SEND)) {
Now we either handle the shared image from onCreate (if the app was just started) or from onNewIntent if an instance was found in memory.
Good luck! If you like this article, please «like» it in social networks, also there are social buttons on a
site itself.