项目3
PROJECT 3
常见花卉识别
本项目基于TensorFlow的4层卷积神经网络进行特征筛选、提取、分类等模型训练工作,对比不同的分类训练效果,选择准确率较高的方式在移动端实现5种花卉的准确识别。
3.1总体设计
本部分包括系统整体结构和系统流程。
3.1.1系统整体结构
系统整体结构如图31所示。
图31系统整体结构
3.1.2系统流程
系统流程如图32所示。
图32系统流程
3.2运行环境
本部分包括Python环境、TensorFlow环境和Android环境。
3.2.1Python环境
需要Python3配置,在Windows环境下进行如下操作:
(1) 下载Python进行安装,下载地址为https://www.python.org/。单击Downloads,进入下载界面。选择版本号 Windows x8664 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
安装成功如图33所示。
图33Android Studio界面
2. 新建Android项目
如果是第一次安装使用,则单击图33中的Start a new Android Studio project 新建项目。反之,打开Android Studio,选择File→New→New Project→Empty Activity→Next, 进行新建项目,如图34所示。
图34配置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/MnisttensorFlowAndroidDemo/tree/master/app/libs。
图35新建armeabiv7a文件夹
将libtensorflow_inference.so放在/app/libs下新建armeabiv7a文件夹中; libandroid_tensorflow_inference_java.jar放在/app/libs下,右击add as Libary,如图35所示。
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会弹出如图36所示的提示。
图36Android 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"
从数据文件夹中读取所有图片名并组织成列表的形式,按训练、验证和测试集分开。再将图片分开后,根据随机得到的一个分数值判断这个图片被分到哪一类数据。带有一定的偶然性,并不能确保有多少张图片属于某一个数据集。读取图片成功示意如图37所示。
图37读取图片成功示意图
对图片进行预处理,例如,将图片名整理成一个字典、获得并返回图片的路径以及计算得到特征向量等。相关代码如下:
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模型训练和InceptionV3模型训练。
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()
图38训练输出结果
训练输出结果如图38 所示。
通过观察训练集和测试集的损失函数、准确率的大小评估模型的训练程度,进行模型训练的进一步决策。
2) InceptionV3模型训练
CNN模型对于花卉的分类准确率大概在70%左右。因此,得出的改进方法为,采用迁移学习调用Inceptionv3模型实现对本文中的花卉数据集分类。
Inception系列解决CNN分类模型的两个问题: ①如何使网络深度增加的同时让模型的分类性能随之增加,而非像简单的VGG网络达到一定深度后就陷入了性能饱和的困惑。②如何在保证分类网络准确率提升或保持不降的同时,使模型的计算开销与内存开销降低。在这个模型中最后一层全连接层之前统称为瓶颈层。Inceptionv3模型下载地址为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
训练输出结果如图39所示。
图39训练输出结果
使用InceptionV3模型的分类准确率在95%左右,准确率得到了较好的改善。经过对比,选择准确率更高的InceptionV3模型进行分类。
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个按钮,控制拍照上传功能
//设置第2个按钮,控制从相册获取照片功能
//设置第3个按钮,控制调用模型进行花卉识别功能
//设置文本,显示预测结果
//设置图片,显示预先设定好的图片、拍照的图片以及相册中导入的图片
该布局文件提供5个控件,3个button,分别是“拍照上传”“从相册获取”“单击输出结果”,1个TextView,显示预测结果,1个ImageView,展示数字图片。
上述代码中android:src="@drawable/test_image"负责设置初始界面的图片显示。将想要显示的名字为test_image的图片放入/res/drawable文件夹中。
2) 模型预测类
相关代码如下:
PredictionTF.java
package com.example.flower;
import android.content.res.AssetManager;
//添加图像处理所需要的头文件
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
//安卓的日志工具
import android.util.Log;
//tensorflow需要的头文件
import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
//添加字典所需要的头文件
import java.util.HashMap;
import java.util.Map;
//预测函数
public class PredictionTF {
private static final String TAG = "PredictionTF";
//设置模型输入/输出节点的数据维度
private static final int IN_COL = 1;
private static final int IN_ROW = 32*64;
//输入的图片类型有5种
private static final int MAXL = 5 ;
//模型中输入变量的名称,必须与训练时的输入变量名称相同
private static final String inputName = "BottleneckInputPlaceholder";
//模型中输出变量的名称,必须与训练时的输出变量名称相同
private static final String outputName = "final_training_ops/prob";
TensorFlowInferenceInterface inferenceInterface;
static {
//加载libtensorflow_inference.so库文件
System.loadLibrary("tensorflow_inference");
Log.e(TAG,"libtensorflow_inference.so库加载成功");
}
PredictionTF(AssetManager assetManager, String modePath) {
//初始化TensorFlowInferenceInterface对象
inferenceInterface = new TensorFlowInferenceInterface(assetManager,modePath);
Log.e(TAG,"TensorFlow模型文件加载成功");
}
/**
* 利用训练好的TensoFlow模型预测结果
* 参数: bitmap 输入被测试的bitmap图
* 返回预测结果,int整数型
*/
Object a;
public Object getPredict(Bitmap bitmap) {
//添加字典,实现输出花卉种类的目的
Map params = new HashMap();
params.put(0,"雏菊");
params.put(1,"蒲公英");
params.put(2,"玫瑰花");
params.put(3,"向日葵");
params.put(4,"郁金香");
//需要将图片缩放到32*64
float[] inputdata = bitmapToFloatArray(bitmap,32,64);
//将数据feed给tensorflow的输入节点
inferenceInterface.feed(inputName, inputdata, IN_COL,IN_ROW);
//运行tensorflow
String[] outputNames = new String[] {outputName};
inferenceInterface.run(outputNames);
//获取输出信息,数据均在0~1,为浮点型
float[] outputs = new float[MAXL];
inferenceInterface.fetch(outputName, outputs);
//查找字典,将outputs对应的花卉名称找到
for (int i = 0;i < 5;i++) {
a=params.get(argMax(outputs));
}
return a;
//return argMax(outputs);
}
/**
* 将bitmap转为(按行优先)float数组,并且每个像素点都归一化到0~1
* 参数: bitmap 输入被测试的bitmap图片
* 参数: rx将图片缩放到指定的大小(列)->32
* 参数: ry将图片缩放到指定的大小(行)->64
* 返回归一化后的一维float数组 ->32*64
*/
public static float[] bitmapToFloatArray(Bitmap bitmap, int rx, int ry){
int height = bitmap.getHeight();
int width = bitmap.getWidth();
//计算缩放比例
float scaleWidth = ((float) rx) / width;
float scaleHeight = ((float) ry) / height;
Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
bitmap=Bitmap.createBitmap(bitmap,0, 0, width, height, matrix, true);
Log.i(TAG,"bitmap width:"+bitmap.getWidth()+",height:"+bitmap.getHeight());
Log.i(TAG,"bitmap.getConfig():"+bitmap.getConfig());
height = bitmap.getHeight();
width = bitmap.getWidth();
float[] result = new float[height*width];
int k = 0;
//行优先
for(int j = 0;j < height;j++){
for (int i = 0;i < width;i++){
int argb = bitmap.getPixel(i,j);
int r = Color.red(argb);
int g = Color.green(argb);
int b = Color.blue(argb);
int a = Color.alpha(argb);
//得到图像灰度
int gray = (int)(r * 0.3 + g * 0.59 + b * 0.11);
result[k++] = gray / 255.0f;
}
}
return result;
}
/**
* 返回数组中最大值的索引
* 参数: output是从TensorFlow模型中取出的output[],一组浮点型数组
* 循环判断数组中的最大值
* 返回最大值的索引,索引值为整型
*/
public int argMax(float[] output){
int maxIndex=0;
for(int i=1; i output[maxIndex]? i: maxIndex;
}
return maxIndex;
}
}
3) 主活动类
相关代码如下:
MainActivity.java
package com.example.flower;
import android.Manifest; //引入AndroidManifest.xml
import android.annotation.TargetApi;
//引入android content命令
import android.content.ContentUris;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor; //数据存储功能
//几何图形处理功能
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri; //不可变的URI引用
import android.os.Build; //获取系统信息
//为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据
import android.provider.DocumentsContract;
import android.provider.MediaStore;
//android中必备的包
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View; //描绘块状视图的基类
//android列表小部件
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
//文件&路径
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
//模型存放路径
private static final String MODEL_FILE = "file:///android_asset/grf.pb";
//设置函数别名
TextView txt;
ImageView imageView;
Bitmap bitmapTest;
PredictionTF preTF;
//定义数据
public static final int TAKE_PHOTO = 1;
public static final int CHOOSE_PHOTO = 2;
private Uri imageUri;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//输入模型存放路径,并加载TensorFlow模型
preTF = new PredictionTF(getAssets(),MODEL_FILE);
//布局控件的 ID绑定
txt = (TextView) findViewById(R.id.txt_id);
imageView = (ImageView) findViewById(R.id.image);
Button takePhotoButton = (Button) findViewById(R.id.take_photo);
Button choosePhotoButton = (Button) findViewById(R.id.from_album);
//拍照按钮的触发事件
takePhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//创建file 对象,用于存储拍照后的照片,命名为output_image.jpg
//getExternalCacheDir()获取手机SD卡的应用关联缓存目录,图片放在该目录下
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if (outputImage.exists()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= 24) {
//如果手机系统版本高于Android7.0
//调用fileProvider的getUriForFile(),将file对象封装成Uri对象,保护数据
imageUri=FileProvider.getUriForFile(MainActivity.this, "com.example.Flower.fileprovider", outputImage);
} else {
//如果手机系统版本低于Android7.0,则直接将File对象转换成Uri对象
//Uri对象就是"output_image.jpg"图片的真实路径
imageUri = Uri.fromFile(outputImage);
}
//启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
});
//相册按钮的触发事件
choosePhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//第一次单击按钮时,需要动态申请WRITE_EXTERNAL_STORAGE权限
//授予程序对SD卡读和写的能力
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
} else {
openAlbum();
}
}
});
}
/**"单击输出结果"按钮的触发事件
* 将ImageView中的图片转换为Bitmap数据
* 该数据作为preTF.getPredict()方法的输入参数
* 得到预测结果,并在TextView中显示
*/
public void clickResult(View v){
String res="预测结果为: ";
bitmapTest =((BitmapDrawable) ((ImageView) imageView).getDrawable()).getBitmap();
Object result = preTF.getPredict(bitmapTest);
res=res+String.valueOf(result)+" ";
txt.setText(res);
}
private void openAlbum(){
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*"); //设置类型为图片
startActivityForResult(intent, CHOOSE_PHOTO);
//调用该方法可以打开相册选择图片
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode){
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
openAlbum();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case TAKE_PHOTO:
if (resultCode == RESULT_OK) {
//如果拍照成功,将照片转换为Bitmap对象,并在控件ImageView中显示
try {
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
imageView.setImageBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
break;
case CHOOSE_PHOTO:
if (resultCode == RESULT_OK){
//选取图片后,根据不同手机系统版本号用不同的方法处理图片
//判断手机系统版本号
if (Build.VERSION.SDK_INT >=19){
//4.4及以上系统使用此方法处理图片
handleImageOnKitKat(data);
} else{
//4.4以下系统用这个方法处理图片
handleImageBeforeKitKat(data);
}
}
default:
break;
}
}
//针对4.4及以上系统,解析图片uri
@TargetApi(19)
private void handleImageOnKitKat(Intent data){
String imagePath = null;
Uri uri = data.getData();
if (DocumentsContract.isDocumentUri(this,uri)){
//如果是document类型的uri 则通过ID进行解析处理
String docId = DocumentsContract.getDocumentId(uri);
if ("com.android.providers.media.documents".equals(uri.getAuthority())){
//media格式,需要再次解析ID,通过分割字符串取出后半部分得到真正的数字ID
String id = docId.split(":")[1];
String selection = MediaStore.Images.Media._ID + "=" +id;
//将新的Uri和条件语句作为参数传入getImagePath(),得到图片的真实路径
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);
}else if ("com.android.providers.downloads.documents".equals(uri.getAuthority()))
{
Uri contentUri = ContentUris.withAppendedId(Uri.parse("" +
"content://downloads/public_downloads"),Long.valueOf(docId));
imagePath = getImagePath(contentUri,null);
}
}else if ("content".equals(uri.getScheme())){
//如果是content类型的uri,则使用普通的方式处理
imagePath = getImagePath(uri,null);
} else if ("file".equalsIgnoreCase(uri.getScheme())){
//如果是file 类的uri,直接获取图片路径即可
imagePath = uri.getPath();
}
//图片路径传入后,调用displayImage() 将图片显示到控件ImageView中
displayImage(imagePath);
}
/**
* 4.4版本以下直接获取uri进行图片处理
* 参数: data
*/
private void handleImageBeforeKitKat(Intent data){
Uri uri = data.getData();
String imagePath = getImagePath(uri,null);
displayImage(imagePath);
}
/**
* 通过 uri seletion选择获取图片的真实uri
* 参数: uri
* 参数: seletion
* 返回图片的真实路径,String形式
*/
private String getImagePath(Uri uri, String seletion){
String path = null;
Cursor cursor = getContentResolver().query(uri,null,seletion,null,null);
if (cursor != null){
if (cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}
/**
* 通过imagepath绘制immageview图像
* 参数: imagPath
*/
private void displayImage(String imagPath){
if (imagPath != null){
Bitmap bitmap = BitmapFactory.decodeFile(imagPath);
imageView.setImageBitmap(bitmap);
}else{
Toast.makeText(this,"图片获取失败",Toast.LENGTH_SHORT).show();
}
}
}
3.4系统测试
本部分包括训练准确率、测试效果及模型应用。
3.4.1训练准确率
CNN模型训练准确率,如图310所示,准确率集中在60%左右。
图310CNN模型准确率
InceptionV3模型训练准确率达到95%+,如图311所示。
图311InceptionV3模型准确率
对比两者的准确率,如图312所示, InceptionV3模型训练准确率相对较高,意味着预测模型训练成功。
图312模型准确率对比
3.4.2测试效果
将数据代入模型进行测试、分类的标签与原始数据进行显示和对比,如图313所示,可以得到验证: 模型可以实现常见花卉的识别。
图313模型训练效果
3.4.3模型应用
本模块包括程序下载运行、应用使用说明和测试结果示例。
1. 程序下载运行
Android项目编译成功后,建议将项目运行到真机上进行测试。模拟器运行较慢而且还要下载插件,不建议使用。运行到真机方法如下:
(1) USB驱动准备。打开AS的SDK Manager,在SDK Tools下勾选Google USB Driver复选框,单击OK按钮。AS会自动下载USB驱动,保存的位置是C:\Users\xxShirley\AppData\Local\Android\Sdk,如图314所示。
图314下载USB驱动
(2) 下载和真机一样版本的SDK ,如果是Android 10.0版本,如图315所示。
图315下载SDK
(3) 安装USB驱动。打开设备管理器,右击移动设备(数据线连接计算机才会有
此选项),选择更新驱动。手动选择驱动,根据上述下载路径找到驱动,安装驱动。
图316连接手机
(4) 打开手机的开发者模式、USB调试、USB安装。
(5) 打开AS状态栏如图316位置的按钮,选择Troubleshoot Device Connections,寻找到自己的设备,将手机与AS相连,有显示手机型号即连接成功。
(6) 单击项目“运行”按钮,Android Studio生成apk,发送到手机,在手机上下载apk,安装即可。
2. 应用使用说明
打开APP,初始界面如图317所示。
单击第三个按钮“输出结果”,可以看到文本框的内容变为“预测结果为: 向日葵”,如图318所示。
图317应用初始界面
图318预测结果显示界面
要找更多的图片进行测试,可单击“拍照上传”或者“从相册获取”按钮。
3. 测试结果示例
移动端测试结果如图319所示。
图319测试结果示例