项目3 PROJECT 3 常见花卉识别 本项目基于TensorFlow的4层卷积神经网络进行特征筛选、提取、分类等模型训练工作,对比不同的分类训练效果,选择准确率较高的方式在移动端实现5种花卉的准确识别。 3.1总体设计 本部分包括系统整体结构和系统流程。 3.1.1系统整体结构 系统整体结构如图31所示。 图31系统整体结构 3.1.2系统流程 系统流程如图32所示。 图32系统流程 3.2运行环境 本部分包括Python环境、TensorFlow环境和Android环境。 3.2.1Python环境 需要Python3配置,在Windows环境下进行如下操作: (1) 下载Python进行安装,下载地址为https://www.python.org/。单击Downloads,进入下载界面。选择版本号 Windows x8664 executable installer进行下载。下载结束后,解压安装包,按照指示进行安装即可使用Python。具体安装步骤可参考教程,网址为https://blog.csdn.net/qq_25814003/article/details/80609729。 (2) 直接下载Anaconda完成Python所需环境配置。Anaconda下载地址为https://www.anaconda.com/,此方式下载较为缓慢,可以在清华开源软件镜像站中下载,下载网址为https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/,选择合适版本,安装步骤与官网下载一致。在Linux环境下,直接下载虚拟机运行代码。 3.2.2TensorFlow环境 安装Anaconda后打开Anaconda Prompt。 (1) 创建TensorFlow环境,与Python版本号进行匹配,输入命令: conda create -n TensorFlow python=3.x x的值根据实际情况决定,创建过程中有需要确认的地方,都输入y。 (2) 激活TensorFlow的环境,输入命令: activate tensorflow 光标前方出现(TensorFlow)则表示创建成功。 (3) 安装TensorFlow: CPU版本安装,输入命令: pip install --ignore-installed --upgrade tensorflow GPU版本安装,输入命令: pip install --ignore-installed --upgrade tensorflow-gpu 不能同时安装CPU和GPU版本的TensorFlow。 以上命令会安装最新版本的TensorFlow,未必需要,建议在后面跟上版本号,安装指定版本的TensorFlow,输入命令: pip install --ignore-installed --upgrade tensorflow==1.4.0 (4) 如果安装错误版本,先卸载之前安装的TensorFlow,输入命令: pip uninstall tensorflow pip uninstall tensorflow-gpu (5) 验证TensorFlow安装成功,输入python进入编程环境,输入命令: import tensorflow as tf hello = tf.constant('Hello, TensorFlow!') sess = tf.Session() print(sess.run(hello)) 运行成功则证明安装正确。 为快速安装,在进行以上步骤前输入清华仓库镜像,在Anaconda Prompt中,输入命令: conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config –set show_channel_urls yes 3.2.3Android环境 本项目使用Android Studio进行开发工作。 1. 安装Android Studio Android Studio下载地址为https://developer.android.google.cn/studio/ 安装参考教程网址为https://developer.android.google.cn/studio/install.html 安装成功如图33所示。 图33Android Studio界面 2. 新建Android项目 如果是第一次安装使用,则单击图33中的Start a new Android Studio project 新建项目。反之,打开Android Studio,选择File→New→New Project→Empty Activity→Next, 进行新建项目,如图34所示。 图34配置Android项目对话框 选择空项目Empty Activity,单击Next按钮。 Name可自行定义,Save location为保存地址,也可自行定义,Language为编码所使用的语言,选择Java、Minimum SDK为该项目能够兼容Android手机的最低版本,保持默认即可。单击Finish按钮,新建项目完成。 导入TensorFlow的jar包和so库: 下载libtensorflow_inference.so、libandroid_tensorflow_inference_java.jar,下载地址为https://github.com/PanJinquan/MnisttensorFlowAndroidDemo/tree/master/app/libs。 图35新建armeabiv7a文件夹 将libtensorflow_inference.so放在/app/libs下新建armeabiv7a文件夹中; libandroid_tensorflow_inference_java.jar放在/app/libs下,右击add as Libary,如图35所示。 app/build.gradle配置,在defaultConfig中添加: ndk { abiFilters "armeabi-v7a" } //在android节点下添加soureSets,用于指定jniLibs的路径 sourceSets { main { jniLibs.srcDirs = ['libs'] } } //在dependencies中(若没有)则增加TensorFlow编译的jar文件: implementation files('libs/libandroid_tensorflow_inference_java.jar') //完整的app/build.gradle 配置代码 apply plugin: 'com.android.application'//表示是一个应用程序的模块,可独立运行 android { compileSdkVersion 28//指定项目的编译版本,真机调试时要与手机系统的版本号对应 buildToolsVersion "29.0.3" //指定项目构建工具的版本 defaultConfig { //引用libtensorflow_inference.so ndk { abiFilters "armeabi-v7a" } applicationId "com.example.flower" //指定包名 minSdkVersion 16 //指定最低兼容的Android系统版本 targetSdkVersion 28//指定目标版本,表示在Android系统版本已经做过充分测试 versionCode 1 //版本号 versionName "1.0" //版本名称 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { //指定生成安装文件的配置 release { //用于指定生成正式版安装文件的配置 minifyEnabled false //指定是否对代码进行混淆,true表示混淆 //指定混淆时使用的规则文件,proguard-android-optimize.txt指所有项目通用的混淆规则,proguard-rules.pro指当前项目特有的混淆规则 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } //在Android节点下添加soureSets,用于指定jniLibs的路径 sourceSets { main { jniLibs.srcDirs = ['libs'] } } } //指定当前项目的所有依赖关系: 本地依赖、库依赖、远程依赖 dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') //本地依赖 implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' //声明测试用例库 androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' implementation files('libs/libandroid_tensorflow_inference_java.jar')// TensorFlow编译 //的jar文件 implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' } app/build.gradle中的内容有任何改动后,Android Studio会弹出如图36所示的提示。 图36Android Studio提示信息 单击Sync Now或图标,即同步该配置,同步成功表示配置完成。 3.3模块实现 本项目包括4个模块: 数据预处理、创建模型并编译、模型训练及保存、模型生成,下面分别给出各模块的功能介绍及相关代码。 3.3.1数据预处理 数据集链接为http://download.tensorflow.org/example_images/flower_photos.tgz,文件夹包含5个子文件,每个子文件夹的名称为一种花,代表不同类别。平均每种花有734张图片,每张图片都是RGB色彩模式,大小不同,程序将直接处理未整理过的图像数据。通过本地导入加载数据集,相关代码如下: #读取图片 def read_img(path): cate=[path+x for x in os.listdir(path) if os.path.isdir(path+x)] imgs=[] labels=[] for idx,folder in enumerate(cate): for im in glob.glob(folder+'/*.jpg'): print('reading the images:%s'%(im)) img=io.imread(im) img=transform.resize(img,(w,h)) imgs.append(img) labels.append(idx) return np.asarray(imgs,np.float32),np.asarray(labels,np.int32) data,label=read_img(path) #打乱顺序 num_example=data.shape[0] arr=np.arange(num_example) np.random.shuffle(arr) data=data[arr] label=label[arr] #将所有数据分为训练集和验证集 ratio=0.8 s=np.int(num_example*ratio) x_train=data[:s] y_train=label[:s] x_val=data[s:] y_val=label[s:] #Inception-v3数据集处理 import glob import os.path import random import numpy as np from tensorflow.python.platform import gfile input_data = "D:/College/Study/3_信息系统设计/flower_photos" 从数据文件夹中读取所有图片名并组织成列表的形式,按训练、验证和测试集分开。再将图片分开后,根据随机得到的一个分数值判断这个图片被分到哪一类数据。带有一定的偶然性,并不能确保有多少张图片属于某一个数据集。读取图片成功示意如图37所示。 图37读取图片成功示意图 对图片进行预处理,例如,将图片名整理成一个字典、获得并返回图片的路径以及计算得到特征向量等。相关代码如下: def create_image_dict(): result = {} #path是flower_photos文件夹的路径,同时也包含了子文件夹的路径 #directory的数据形式为一个列表 #/flower_photos,/flower_photos/daisy, #/flower_photos/tulips, flower_photos/roses, #/flower_photos/dandelion, /flower_photos/sunflowers path_list = [x[0] for x in os.walk(input_data)] is_root_dir = True for sub_dirs in path_list: if is_root_dir: is_root_dir = False continue#continue会跳出当前循环执行下一轮的循环 #extension_name列出了图片文件可能的扩展名 extension_name = ['jpg', 'jpeg', 'JPG', 'JPEG'] #创建保存图片文件名的列表 images_list = [] for extension in extension_name: #join()函数用于拼接路径,用extension_name列表中的元素作为后缀名 #/flower_photos/daisy/*.jpg #/flower_photos/daisy/*.jpeg #/flower_photos/daisy/*.JPG #/flower_photos/daisy/*.JPEG file_glob = os.path.join(sub_dirs, '*.' + extension) #使用glob()函数获取满足正则表达式的文件名 #/flower_photos/daisy/*.jpg,glob()函数会得到该路径下 #所有后缀名为.jpg的文件 #/flower_photos/daisy/7924174040_444d5bbb8a.jpg images_list.extend(glob.glob(file_glob)) #basename()函数会舍弃一个文件名中保存的路径 #/flower_photos/daisy,其结果是仅保留daisy #flower_category是图片的类别,通过子文件夹名获得 dir_name = os.path.basename(sub_dirs) flower_category = dir_name #初始化每个类别flower photos对应的训练集图片名列表、测试集图片名列表 #和验证集图片名列表 training_images = [] testing_images = [] validation_images = [] for image_name in images_list: #对于images_name列表中的图片文件名,也包含了路径名 #使用basename()函数获取文件名 image_name = os.path.basename(image_name) #random.randint()函数产生均匀分布的整数 score = np.random.randint(100) if score < 10: validation_images.append(image_name) elif score < 20: testing_images.append(image_name) else: training_images.append(image_name) #每执行一次最外层的循环,都会刷新一次result,result是一个字典 #它的key为flower_category,value也是一个字典,以数据集分类的形式存储所有图片的 #名称,最后函数将结果返回 result[flower_category] = { "dir": dir_name, "training": training_images, "testing": testing_images, "validation": validation_images, } return result def get_image_path(image_lists, image_dir, flower_category, image_index, data_category): #根据传递进来的参数返回一个带路径的图片名 #category_list用列表的形式保存了某一类花的某一个数据集内容 #其中参数flower_category从函数get_random_bottlenecks()传递过来 category_list = image_lists[flower_category][data_category] #actual_index是一个图片在category_list列表中的位置序号 #其中参数image_index也是从函数get_random_bottlenecks()传递过来 actual_index = image_index % len(category_list) #image_name就是图片的文件名 image_name = category_list[actual_index] #sub_dir得到flower_photos中某一类花所在的子文件夹名 sub_dir = image_lists[flower_category]["dir"] #拼接路径,这个路径包含了文件名,最终返回给create_bottleneck()函数 #作为每一个图片对应的特征向量的文件 full_path = os.path.join(image_dir, sub_dir, image_name) return full_path def create_bottleneck(sess, image_lists,flower_category, image_index, data_category, jpeg_data_tensor, bottleneck_tensor): ''' 获取一张图片经过inception V3模型处理之后的特征向量。在获取特征向量时,先在CACHR_DIR路径下寻找已经计算且保存下来的特征向量并读取。首先,将其内容作为列表返回,如果找不到该文件则通过inception V3模型计算特征向量; 其次,计算得到的特征向量保存到文件(.txt),最后返回计算得到的特征向量列表。 ''' #sub_dir得到的是flower_photos下某一类花的文件夹名 #flower_photos参数确定,花的文件夹名由dir参数确定 sub_dir = image_lists[flower_category]["dir"] #拼接路径,路径名就是在CACHE_DIR的基础上加sub_dir sub_dir_path = os.path.join(CACHE_DIR, sub_dir) #判断拼接出的路径是否存在,如果不存在,则在CACHE_DIR下创建相应的子文件夹 if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path) #获取一张图片对应特征向量的全名,全名包括路径名,而且会在图片.jpg后面 #用.txt作为后缀,获取没有.txt后缀的文件名使用了get_image_path()函数,该函 #数会返回带路径的图片名 bottleneck_path = get_image_path(image_lists, CACHE_DIR, flower_category,image_index, data_category) + ".txt" #如果指定名称的特征向量文件不存在,则通过InceptionV3模型计算得到该特征向量 #计算的结果也会存入文件 if not os.path.exists(bottleneck_path): #获取原始的图片名,这个图片名包含了原始图片的完整路径 image_path = get_image_path(image_lists, input_data, flower_category,image_index, data_category) #读取图片的内容 image_data = gfile.FastGFile(image_path, "rb").read() #将当前图片输入InceptionV3模型,并计算瓶颈张量的值 #所得瓶颈张量的值就是特征向量,但是得到的特征向量是四维的,所以还需要通过 #squeeze()函数压缩成一维的,以方便作为全连层的输入 bottleneck_values = sess.run(bottleneck_tensor, feed_dict={jpeg_data_tensor: image_data}) bottleneck_values = np.squeeze(bottleneck_values) #将计算得到的特征向量存入文件,存之前需要在两个值之间加入逗号作为分隔, #方便从文件读取数据时的解析过程 bottleneck_string=','.join(str(x) for x in bottleneck_values) with open(bottleneck_path, "w") as bottleneck_file: bottleneck_file.write(bottleneck_string) else: #else是特征向量文件已经存在的情况,会直接从bottleneck_path获取数据 with open(bottleneck_path, "r") as bottleneck_file: bottleneck_string = bottleneck_file.read() #从文件读取的特征向量数据是字符串的形式,要以逗号为分隔将其转为列表的形式 bottleneck_values=[float(x) for x in bottleneck_string.split(',')] return bottleneck_values def get_random_bottlenecks(sess,num_classes,image_lists, batch_size, data_category, jpeg_data_tensor,bottleneck_tensor): #随机产生一个batch的特征向量及其对应的labels #定义bottlenecks用于存储,得到batch的特征向量 #定义labels用于存储batch的label标签 bottlenecks = [] labels = [] for i in range(batch_size): #random_index从五种花中随机抽取类别编号 #image_lists.keys()值就是五种花的类别名称 random_index = random.randrange(num_classes) flower_category = list(image_lists.keys())[random_index] #image_index是随机抽取图片的编号,在get_image_path()函数中 #如何通过图片编号和random_index确定类别找到图片的文件名 image_index = random.randrange(65536) #调用get_or_create_bottleneck()函数获取或者创建图片的特征向量 #调用get_image_path()函数 bottleneck=create_bottleneck(sess,image_lists, flower_category,image_index,data_category, jpeg_data_tensor, bottleneck_tensor) #生成每一个标签的答案值,通过append()函数组织成一个batch列表 #函数将完整的列表返回 label = np.zeros(num_classes, dtype=np.float32) label[random_index] = 1.0 labels.append(label) bottlenecks.append(bottleneck) return bottlenecks, labels def get_test_bottlenecks(sess, image_lists, num_classes, jpeg_data_tensor, bottleneck_tensor): ''' 获取全部的测试数据。用两个for循环遍历所有用于测试的花名,并根据create_bottlenecks()函数获取特征向量数据 ''' bottlenecks = [] labels = [] #flower_category_list是image_lists中键的列表 #['roses', 'sunflowers', 'daisy', 'dandelion', 'tulips'] flower_category_list = list(image_lists.keys()) data_category = "testing" #枚举所有的类别和每个类别中的测试图片 #在外层的for循环中,label_index是flower_category_list列表中的元素下标 #flower_category就是该列表中的值 for label_index, flower_category in enumerate(flower_category_list): #在内层的for循环中,通过flower_category和testing枚举每一种花 #用于测试的花名,得到的名字就是unused_base_name,但只需image_index For image_index,unused_base_name in enumerate(image_lists[flower_category] ["testing"]): #调用create_bottleneck()函数创建特征向量,在进行训练或验证的过程中 #用于测试的图片并没有生成相应的特征向量,所以要一次性全部生成 bottleneck = create_bottleneck(sess, image_lists, flower_category, image_index,data_category, jpeg_data_tensor, bottleneck_tensor) #与get_random_bottlenecks()函数相同 label = np.zeros(num_classes, dtype=np.float32) label[label_index] = 1.0 labels.append(label) bottlenecks.append(bottleneck) return bottlenecks, labels x = tf.placeholder("float32", shape=[None, 784],name='x') y_ = tf.placeholder("float32", shape=[None, 10],name='y_') 3.3.2创建模型并编译 数据加载进模型后,需要定义模型结构并优化损失函数及模型。 1. 定义模型结构 定义的架构为4个卷积层,在每层卷积后都连接1个池化层,进行数据的降维,3个全连接层和1个Softmax层。在每层卷积层上使用多个滤波器提取不同类型的特征。最大池化和全连接层之后,在模型中引入丢弃进行正则化,用以消除模型的过拟合问题。 x=tf.placeholder(tf.float32,shape=[None,w,h,c],name='x') y_=tf.placeholder(tf.int32,shape=[None,],name='y_') #定义函数inference,定义CNN网络结构 #卷积神经网络,卷积加池化*4,全连接*3,softmax分类 #卷积层1 def inference(input_tensor, train, regularizer): with tf.variable_scope('layer1-conv1'): conv1_weights = tf.get_variable("weight",[5,5,3,32],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv1_biases = tf.get_variable("bias", [32], initializer=tf.constant_initializer(0.0)) conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME') relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases)) #池化层1 #2*2最大池化,步长strides为2,池化后执行lrn()操作,局部响应归一化,对训练有利 with tf.name_scope("layer2-pool1"): pool1 = tf.nn.max_pool(relu1, ksize = [1,2,2,1],strides=[1,2,2,1],padding="VALID") #卷积层2 #64个5*5的卷积核(16通道) #padding='SAME',表示padding后卷积的图与原图尺寸一致,激活函数relu() with tf.variable_scope("layer3-conv2"): conv2_weights = tf.get_variable("weight",[5,5,32,64],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv2_biases = tf.get_variable("bias", [64], initializer=tf.constant_initializer(0.0)) conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME') relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases)) #池化层2 #2*2最大池化,步长strides为2,池化后执行lrn()操作 with tf.name_scope("layer4-pool2"): pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID') #卷积层3 #128个3*3的卷积核(64通道) with tf.variable_scope("layer5-conv3"): conv3_weights = tf.get_variable("weight",[3,3,64,128],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv3_biases = tf.get_variable("bias", [128], initializer=tf.constant_initializer(0.0)) conv3 = tf.nn.conv2d(pool2, conv3_weights, strides=[1, 1, 1, 1], padding='SAME') relu3 = tf.nn.relu(tf.nn.bias_add(conv3, conv3_biases)) #池化层3 #2*2最大池化,步长strides为2,池化后执行lrn()操作 with tf.name_scope("layer6-pool3"): pool3 = tf.nn.max_pool(relu3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID') #卷积层4 #128个3*3的卷积核(128通道) with tf.variable_scope("layer7-conv4"): conv4_weights=tf.get_variable("weight",[3,3,128,128],initializer=tf.truncated_normal_initializer(stddev=0.1)) conv4_biases = tf.get_variable("bias", [128], initializer=tf.constant_initializer(0.0)) conv4=tf.nn.conv2d(pool3,conv4_weights,strides=[1,1,1,1], padding='SAME') relu4 = tf.nn.relu(tf.nn.bias_add(conv4, conv4_biases)) #池化层4 #2*2最大池化,步长strides为2,池化后执行lrn()操作 with tf.name_scope("layer8-pool4"): pool4 = tf.nn.max_pool(relu4, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID') nodes = 6*6*128 reshaped = tf.reshape(pool4,[-1,nodes]) #全连接层1 #拥有1024个神经元 with tf.variable_scope('layer9-fc1'): fc1_weights = tf.get_variable("weight", [nodes, 1024], initializer=tf.truncated_normal_initializer(stddev=0.1)) if regularizer != None: tf.add_to_collection('losses', regularizer(fc1_weights)) fc1_biases = tf.get_variable("bias", [1024], initializer=tf.constant_initializer(0.1)) fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases) if train: fc1 = tf.nn.dropout(fc1, 0.5) #全连接层2 #拥有512个神经元 with tf.variable_scope('layer10-fc2'): fc2_weights = tf.get_variable("weight", [1024, 512], initializer=tf.truncated_normal_initializer(stddev=0.1)) if regularizer != None: tf.add_to_collection('losses', regularizer(fc2_weights)) fc2_biases = tf.get_variable("bias", [512], initializer=tf.constant_initializer(0.1)) fc2 = tf.nn.relu(tf.matmul(fc1, fc2_weights) + fc2_biases) if train: fc2 = tf.nn.dropout(fc2, 0.5) #全连接层3 #拥有5个神经元 with tf.variable_scope('layer11-fc3'): fc3_weights = tf.get_variable("weight", [512, 5], initializer=tf.truncated_normal_initializer(stddev=0.1)) if regularizer != None: tf.add_to_collection('losses', regularizer(fc3_weights)) fc3_biases = tf.get_variable("bias", [5], initializer=tf.constant_initializer(0.1)) logit = tf.matmul(fc2, fc3_weights) + fc3_biases return logit 2. 优化损失函数及模型 确定模型架构后进行编译,这是多类别的分类问题,因此,需要使用交叉熵作为损失函数。由于所有的标签都带有相似的权重,经常使用精确度作为性能指标。Adam是常用的梯度下降方法,使用它来优化模型参数。 #定义损失函数和优化器 loss=tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=y_) train_op=tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss) correct_prediction = tf.equal(tf.cast(tf.argmax(logits,1),tf.int32), y_) acc= tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) 3.3.3模型训练及保存 在定义模型架构和编译之后,通过训练集训练模型,识别花卉。这里,将使用训练集和测试集拟合、改进,并保存模型。 1. 模型训练 本部分包括CNN模型训练和InceptionV3模型训练。 1) CNN模型训练 CNN模型是本项目对于花卉分类的基本模型,相关代码如下: n_epoch=10 batch_size=64 saver=tf.train.Saver() #产生一个会话 sess=tf.Session() #所有节点初始化 sess.run(tf.global_variables_initializer()) for epoch in range(n_epoch): start_time = time.time() #训练数据及标签 train_loss, train_acc, n_batch = 0, 0, 0 #打印准确率 for x_train_a, y_train_a in minibatches(x_train, y_train, batch_size, shuffle=True): _,err,ac=sess.run([train_op,loss,acc], feed_dict={x: x_train_a, y_: y_train_a}) train_loss += err; train_acc += ac; n_batch += 1 print(" train loss: %f" % (np.sum(train_loss)/ n_batch)) print(" train acc: %f" % (np.sum(train_acc)/ n_batch)) #验证 val_loss, val_acc, n_batch = 0, 0, 0 for x_val_a, y_val_a in minibatches(x_val, y_val, batch_size, shuffle=False): err, ac = sess.run([loss,acc], feed_dict={x: x_val_a, y_: y_val_a}) val_loss += err; val_acc += ac; n_batch += 1 print(" validation loss: %f" % (np.sum(val_loss)/ n_batch)) print(" validation acc: %f" % (np.sum(val_acc)/ n_batch)) saver.save(sess,model_path) sess.close() 图38训练输出结果 训练输出结果如图38 所示。 通过观察训练集和测试集的损失函数、准确率的大小评估模型的训练程度,进行模型训练的进一步决策。 2) InceptionV3模型训练 CNN模型对于花卉的分类准确率大概在70%左右。因此,得出的改进方法为,采用迁移学习调用Inceptionv3模型实现对本文中的花卉数据集分类。 Inception系列解决CNN分类模型的两个问题: ①如何使网络深度增加的同时让模型的分类性能随之增加,而非像简单的VGG网络达到一定深度后就陷入了性能饱和的困惑。②如何在保证分类网络准确率提升或保持不降的同时,使模型的计算开销与内存开销降低。在这个模型中最后一层全连接层之前统称为瓶颈层。Inceptionv3模型下载地址为https://storage.googleapis.com/download.tensorflow.org/models/inception_dec_2015.zip,相关代码如下: import tensorflow as tf import os import flower_photos_dispose as fd from tensorflow.python.platform import gfile model_path = "D:/College/Study/3_信息系统设计/inception_dec_2015" model_file = "tensorflow_inception_graph.pb" num_steps = 4000 BATCH_SIZE = 100 bottleneck_size = 2048#InceptionV3模型瓶颈层的节点个数 #调用create_image_lists()函数获得该函数返回的字典 image_lists = fd.create_image_dict() num_classes = len(image_lists.keys())# num_classes=5,因为有5类 #读取已经训练好的Inception-v3模型 with gfile.FastGFile(os.path.join(model_path, model_file), 'rb') as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) #使用import_graph_def()函数加载读取的InceptionV3模型 #返回图像数据输入节点的张量名称以及计算瓶颈结果所对应的张量 bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(graph_def, return_elements=["pool_3/_reshape:0", "DecodeJpeg/contents:0"]) x=tf.placeholder(tf.float32,[None,bottleneck_size], name='BottleneckInputPlaceholder') y_=tf.placeholder(tf.float32,[None, num_classes], name='GroundTruthInput') #定义一层全连接层 with tf.name_scope("final_training_ops"): weights=tf.Variable(tf.truncated_normal([bottleneck_size, num_classes], stddev=0.001)) biases = tf.Variable(tf.zeros([num_classes])) logits = tf.matmul(x, weights) + biases final_tensor = tf.nn.softmax(logits, name='prob') 定义交叉熵损失函数以及train_step使用的随机梯度下降优化器 cross_entropy=tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y_) cross_entropy_mean = tf.reduce_mean(cross_entropy) train_step= tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy_mean) #定义计算正确率的操作 correct_prediction= tf.equal(tf.argmax(final_tensor, 1), tf.argmax(y_, 1)) evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32)) with tf.Session() as sess: init = tf.global_variables_initializer() sess.run(init) for i in range(num_steps): #使用get_random_bottlenecks()函数产生训练用的随机特征向量数据及其对应的标签 #在run()函数内开始训练的过程 train_bottlenecks, train_labels = fd.get_random_bottlenecks(sess, num_classes, image_lists, BATCH_SIZE, "training", jpeg_data_tensor, bottleneck_tensor) sess.run(train_step,feed_dict={x:train_bottlenecks,y_: train_labels}) #进行验证,使用get_random_bottlenecks()函数产生随机的特征向量及其对应标签 if i % 100 == 0: validation_bottlenecks,validation_labels=fd.get_random_bottlenecks(sess, num_classes,image_lists,BATCH_SIZE,"validation",jpeg_data_tensor, bottleneck_tensor) validation_accuracy = sess.run(evaluation_step, feed_dict={ x: validation_bottlenecks, y_: validation_labels}) print("Step%d:Validationaccuracy=%.1f%%"%(i,validation_accuracy * 100)) #在最后的测试数据上测试正确率,这里调用的是get_test_bottlenecks()函数 #返回所有图片的特征向量作为特征数据 test_bottlenecks,test_labels=fd.get_test_bottlenecks(sess, image_lists, num_classes, jpeg_data_tensor, bottleneck_tensor) test_accuracy=sess.run(evaluation_step, feed_dict={x: test_bottlenecks, y_: test_labels}) print("Finally test accuracy = %.1f%%" % (test_accuracy * 100)) from tensorflow.python.fram 训练输出结果如图39所示。 图39训练输出结果 使用InceptionV3模型的分类准确率在95%左右,准确率得到了较好的改善。经过对比,选择准确率更高的InceptionV3模型进行分类。 2. 模型保存 为能够被Android程序读取,需要将模型文件保存为.pb格式,利用TensorFlow中的graph_util模块进行模型保存。 from tensorflow.python.framework import graph_util #保存为.pb文件 constant_graph = graph_util.convert_variables_to_constants(sess, sess.graph_def,[ " final_training_ops /prob"]) with tf.gfile.FastGFile('grf.pb', mode='wb') as f: f.write(constant_graph.SerializeToString()) 模型被保存后,可以被重用,也可以移植到其他环境中使用。 3.3.4模型生成 该测试分两部分: 一是移动端(以Android为例)调用摄像头和相册获取数字图片; 二是将数字图片转换为数据,输入TensorFlow的模型中,并且获取输出。 1. 权限注册 权限注册相关操作步骤如下。 (1) 调用摄像头需要注册内容提供器,对数据进行保护。在Android Manifest.xml中注册,相关代码如下: android: name属性值是固定的(若targetSDKversion为29,该属性应为"androidx.core.content.FileProvider"),android: authorities属性的值必须和FileProvider.getUriForFile()方法中的第二个参数一致。 另外,的resource属性需自行创建,右击res目录→New→Directory,创建xml目录; 右击xml目录→New→File,创建file_paths.xml文件。修改file_paths.xml文件中的内容,相关代码如下: //若为空就共享整个SD卡,也可以写具 //体的新建文件路径 (2) 调用摄像头需要访问SD卡的应用关联目录。在Android 4.4系统之前,访问SD卡的应用关联目录要声明权限。为了兼容旧版本系统,需要在AndroidManifest.xml中增加访问SD卡的权限。 //关联 //目录的权限 (3) 调用手机相册时需要动态申请WRITE_EXTERNAL_STORAGE这个危险权限,该权限表示同时授予程序对SD卡读和写的能力。 (4) 不同版本的手机,在处理图片上方法不同。因为Android系统从4.4版本开始,选取相册中的图片不再返回真实的Uri,而是封装过的,因此,如果是4.4版本以上的手机需要对Uri进行解析,调用handleImageOnKitKat()方法处理图片,否则调用handleImageBeforeKitKat()。 2. 模型导入及调用 模型导入相关操作步骤如下: (1) 把训练好的.pb文件放入Android项目app/src/main/assets下,若不存在assets目录,右击main→new→Directory,输入assets。 (2) 新建类PredictionTF.java,在该类中加载so库,调用TensorFlow模型得到预测结果。 (3) 在MainActivity.java中声明模型存放路径,调用PredictionTF类。 private static final String MODEL_FILE = "file:///android_asset/grf.pb"; //模型存放路径 preTF =new PredictionTF(getAssets(),MODEL_FILE); //输入模型存放路径,并加载TensorFlow模型 /**"单击输出结果"按钮的触发事件 *将ImageView中的图片转换为Bitmap数据 * 该数据作为preTF.getPredict()方法的输入参数 * 得到预测结果,并在TextView中显示 */ public void clickResult(View v){ String res="预测结果为: "; bitmapTest=((BitmapDrawable)((ImageView) imageView).getDrawable()).getBitmap(); int result = preTF.getPredict(bitmapTest); res=res+String.valueOf(result)+" "; txt.setText(res); } 3. 相关代码 本部分包括布局文件、模型预测类和主活动类。 1) 布局文件 相关代码如下: /res/layout/activity_main.xml //线性布局,从上到下 //设置第1个按钮,控制拍照上传功能