迁移学习InceptionV3

手动画过AlexNet类似的模型,直接在后面加了一层[,class]的全连接层, 结果预测准确率是90%左右. 当然不是很满意. 于是乎开始各种方法寻求最准的CNN
偶然间发现通过迁移学习大赛上高分的作品,进行迁移学习可以让我们拥有更精准的准确率,于是有了本文.
总结:: 学会如何迁移优秀的模型来为自己所用!
文章讲述的colab源码已经共享, 请点击下方链接查看即可.
https://drive.google.com/file/d/1ahJtJvuHDGh4oS4lyNG0NdtqZuyns4J6/view?usp=sharing

迁移准备

模型选择准备

首先当然是选择一款高效的模型,查阅了部分资料, 发现googleNet是2014年ILSVRC挑战赛获得冠军, 错误率降低到6.67%, 另一个名字也是InceptionV1, V3就是它优化后的第三代, 错误率更低
模型下载:
https://storage.googleapis.com/download.tensorflow.org/models/inception_dec_2015.zip 并解压得到.pd文件

执行下方代码, 可以打印出pb文件的全部节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tf_model_path = '/Users/dobby/Desktop/inception_dec_2015/tensorflow_inception_graph.pb'
with open(tf_model_path, 'rb') as f:
serialized = f.read()
tf.reset_default_graph()
original_gdef = tf.GraphDef()
original_gdef.ParseFromString(serialized)
##2 可以在这里查看到全部的图信息(前提是你有转换成功)
with tf.Graph().as_default() as g:
tf.import_graph_def(original_gdef, name='')
ops = g.get_operations()
try:
for i in range(10000):
print('op id {} : op name: {}, op type: "{}"'.format(str(i),ops[i].name, ops[i].type))
except:
pass

需要关注的输入层:

和需要被修改最后一层的全连接层:

我们需要接入到这一层, 然后修改后面的node, 修改这一层原本是[2048, 1000] , 现在需要为我们自己的[2048, n_classes] , 我们自己需要分类的大小

训练环境准备

训练环境我选择的是colab, google的免费GPU, 部署上传下载到colab可以见:https://paulswith.github.io/2018/02/01/%E9%83%A8%E7%BD%B2TensorFlow%E5%88%B0Colaboratory/

免费的GPU当然很快, 但是坑超巨多, 如何你的VPN不稳定, 就很容易导致断连, 数据模型无法被保存,所以我封装一个小代码块, 在训练区间,保存之后调该方法上传到google-drive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def colab_to_drive(file_path, save_name):
'''
** 从Colab到drive **
'''
auth.authenticate_user()
drive_service = build('drive', 'v3')
file_metadata = {
'name': save_name,
'mimeType': 'text/plain'
}
media = MediaFileUpload(file_path,
mimetype='text/plain',
resumable=True)
drive_service.files().create(body=file_metadata,
media_body=media,
fields='id').execute()
print("传输完成.")
def upload_demol():
'''
** 封装上传 **
'''
time_str = datetime.now().strftime('_%d_%H_%M_%S')
name = 'RSM_'+time_str+'.gz'
tar_cmd = 'tar zcvf {a} {b}'.format(a=name, b=MODEL_SAVE_DIR)
rm_cmd = 'rm -rf {}'.format(name)
get_ipython().system(tar_cmd)
colab_to_drive(name, name)
get_ipython().system(rm_cmd)
print("上传成功,包名为",name)
# upload_demol()

hardCode训练

当然方法一开始我也是模糊的, 代码参考来自:
http://www.cnblogs.com/hellcat/p/6909269.html
十分感谢前人种树, 方法基本是通用的, 让我学会了如何迁移其他的模型


但是跑起来是有地方会报错的, 做了修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def get_test_bottlenecks(sess,image_lists,n_class,jpeg_data_tensor,bottleneck_tensor):
'''
获取全部的测试数据,计算输出
:param sess:
:param image_lists:
:param n_class:
:param jpeg_data_tensor:
:param bottleneck_tensor:
:return: 瓶颈输出 & label
'''
bottlenecks = []
ground_truths = []
label_name_list = list(image_lists.keys())
# ** 原先方法会报错:
# for label_index,label_name in enumerate(image_lists[label_name_list]):
# **修改如下(3行):
label_index = random.randrange(n_class) # 标签索引随机生成
label_name = label_name_list[label_index]
label_index= label_name_list.index(label_name)
category = 'testing'
for index, unused_base_name in enumerate(image_lists[label_name][category]): # 索引, {文件名}
bottleneck = get_or_create_bottleneck(
sess, image_lists, label_name, index,
category, jpeg_data_tensor, bottleneck_tensor)
ground_truth = np.zeros(n_class, dtype=np.float32)
ground_truth[label_index] = 1.0
bottlenecks.append(bottleneck)
ground_truths.append(ground_truth)
return bottlenecks, ground_truths

迁移方法

几个核心的方法mark下:

加载获取用得到的tensor

我们需要获取 图片的输入-> 想断点的这两个节点的tensor

1
2
3
4
5
6
7
8
9
10
# 1. 图加载模型
with open(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
# 2.导入图, 且从图上读取tensor, return_elements=[BOTTLENECK_TENSOR_NAME,JPEG_DATA_TENSOR_NAME]) 让返回这个tensor
# 方便我们runsession ,返回后续tensor
bottleneck_tensor,jpeg_data_tensor = tf.import_graph_def(
graph_def,
return_elements=[BOTTLENECK_TENSOR_NAME,JPEG_DATA_TENSOR_NAME])

拿到断点tensor

上面我们获取到了输入tensor和断点tensor, 操作的步骤如同我们直接调用它的模型是一样的, 按照它们先前的格式将图片输入,然后然后它在我们想要的断点节点返回内容

1
2
3
4
5
6
7
8
9
10
11
12
13
def run_bottleneck_on_images(sess,image_data,jpeg_data_tensor,bottleneck_tensor):
'''
使用加载的训练好的Inception-v3模型处理一张图片,得到这个图片的特征向量。
:param sess: 会话句柄
:param image_data: 图片文件句柄
:param jpeg_data_tensor: 输入张量句柄
:param bottleneck_tensor: 瓶颈张量句柄
:return: 瓶颈张量值
'''
bottleneck_values = sess.run(bottleneck_tensor,feed_dict={jpeg_data_tensor:image_data})
print(bottleneck_values.shape) # 从这里也能知道它返回的shape
bottleneck_values = np.squeeze(bottleneck_values)
return bottleneck_values

定义新网络

从上面拿到了需要断点的tensor, 它返回shape是[None, 2048], 第一坑placeholder留给它
第二个placeholder, 是我们的label, 只需要定义为[None, n_classes]

1
2
3
4
5
6
7
8
9
10
bottleneck_input = tf.placeholder(tf.float32, [None,BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
ground_truth_input = tf.placeholder(tf.float32, [None,n_class], name='GroundTruthInput')
# 上面的操作后,我们定义好自己的全连接层,接入即可
with tf.name_scope('final_train_ops'):
# Weight-bias初始化
Weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE,n_class],stddev=0.001))
biases = tf.Variable(tf.zeros([n_class]))
logits = tf.matmul(bottleneck_input,Weights) + biases
final_tensor = tf.nn.softmax(logits, name='softMax_last')
tf.add_to_collection(name='final_tensor', value=final_tensor)

定义常规的优化器

这一步都是通用的, pass哈

1
2
3
4
5
6
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits,labels=ground_truth_input))
train_step = tf.train.RMSPropOptimizer(LEARNING_RATE).minimize(cross_entropy)
# 正确率
with tf.name_scope('evaluation'):
correct_prediction = tf.equal(tf.argmax(final_tensor,1,name='argmax_softMax_last'),tf.argmax(ground_truth_input,1))
evaluation_step = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

踩过的坑

batchSize

我做的是101分类, batchSize对于梯队下降是起到很重要的作用的, 但对于定义多少却是个难题(因为之前都是10分类大小的, 大多定义batchSize=30太随意), 为什么这么关注:
batch数太小,而类别又比较多的时候,真的可能会导致loss函数震荡而不收敛,尤其是在你的网络比较复杂的时候
batch太大, 导致内存利用紧张
最后解决方法, 参考文章:
https://www.zhihu.com/question/32673260
最后选择的是batchSize = 101

国外网络不稳定,导致容易掉线

因为colab是需要墙外访问, 当网络不稳定断开的时候, 我们重新进入环境就必须重启它, 导致跑着的变量代码必须重新启动. 这是很难办.最好用两个方法解决:
1, 让数据接着跑:

1
2
3
4
5
6
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
if keepon:
print('继续加载模型:')
saver.restore(sess, tf.train.latest_checkpoint(MODEL_SAVE_DIR))

good job

2, 善于保存,善于上传到driver

1
2
3
4
5
if i % 1000 == 0:
print("进行存储上传")
tf.train.write_graph(sess.graph, MODEL_SAVE_DIR, 'model.pbtxt')
upload_demol()
saver.save(sess, MODEL_SAVEPATH, global_step=50) # 50次保存一次

优化器的坑

如果你查看了上面blog的作者, 他使用的:GradientDescentOptimizer, 跑了几万次迭代后, 容易陷入局部参数. 当然也有进行过, placeHolder-LR, 但是情况还不是很乐观, 可能GSD确实不适合我.
你可以看到我最后选择的RMSPropOptimizer, 这是一类自适应学习率, 参考文章:
http://ycszen.github.io/2016/08/24/SGD%EF%BC%8CAdagrad%EF%BC%8CAdadelta%EF%BC%8CAdam%E7%AD%89%E4%BC%98%E5%8C%96%E6%96%B9%E6%B3%95%E6%80%BB%E7%BB%93%E5%92%8C%E6%AF%94%E8%BE%83/

替换为RMS后的效果很是不错(之所以有部分高震荡, 是因为断网重连后, 它还没适应下去,所以loss依然很高, 正常现象除非你网络稳定,loss也会很好看~)

加载模型识别

识别率基本是满足的了, 达到了95%以上, 下一步是迁移到IOS的APP上, 拿张图片show一下看准不准:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# -*- coding:utf-8 -*-
__author = ''
import tensorflow as tf
import numpy as np
# ***********************define***********************
MODEL_PATH = '/Users/dobby/TEMP/SaveData/model/101_class_model.ckpt-50.meta'
MODEL_DIR = '/Users/dobby/TEMP/SaveData/model'
image_path = '/Users/dobby/Desktop/pisa.jpeg'
CATO_PATH = '/Users/dobby/TEMP/SaveData/model/catogory.info'
catogo_list = open(CATO_PATH, 'r').read().split('|')
# ***********************LOADGraph***********************
sess = tf.Session()
saver = tf.train.import_meta_graph(MODEL_PATH)
saver.restore(sess, tf.train.latest_checkpoint(MODEL_DIR))
graph = tf.get_default_graph()
# ***********************Define-Node***********************
inpiut_x = graph.get_tensor_by_name('import/DecodeJpeg/contents:0')
poo3 = graph.get_tensor_by_name('import/pool_3/_reshape:0')
change_input = graph.get_tensor_by_name('BottleneckInputPlaceholder:0')
predict = graph.get_tensor_by_name('final_train_ops/softMax_last:0')
print('加载模型成功')
def classification_photo():
# step 打开图片切换格式
image = tf.gfile.FastGFile(image_path, 'rb').read() # inceptionV3不需要转换图片, 它自己换处理图片
poo3_frist = sess.run(poo3, feed_dict={inpiut_x: image}) # 按照模型的顺序要, 先喂给它图片, 然后图片提取到瓶颈的tensor
result = sess.run(predict, feed_dict={change_input:poo3_frist}) # 瓶颈的tensor再转入input传入, 得到我们最后的predict
ar = np.array(result).flatten().tolist()
inde = ar.index(max(ar))
print("it's a {} photo".format(catogo_list[inde]))
classification_photo()

预告

将训练好的模型移动到移动端, 基本已经完成,期待更新吧.

© 2020 In 'flight of thought' All Rights Reserved. 本站访客数人次 本站总访问量
Theme by hiero