手动画过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 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
|
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
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
|
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
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
|
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
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 11 12 13 14 15 16 17 18 19 20 21 22 23
|
1
2
3
4
5
6
7
8
9
10
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
\# 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 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 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 11 12 13 14 15 16 17 18 19 20 21 22 23
|
1
2
3
4
5
6
7
8
9
10
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
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 7 8 9 10 11 12 13 14 15
|
1
2
3
4
5
6
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
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 7 8 9 10 11 12 13 14 15
|
1
2
3
4
5
6
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
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 6 7 8 9 10 11 12 13
|
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
|
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
|
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
\# -\*- 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()
|

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