迁移学习-使用预训练的Inception v3进行宠物分类

迁移学习

    迁移学习(Transfer Learning)是一种机器学习方法,就是把为任务 A 开发的模型作为初始点,重新使用在为任务 B 开发模型的过程中。迁移学习是通过从已学习的相关任务中转移知识来改进学习的新任务,虽然大多数机器学习算法都是为了解决单个任务而设计的,但是促进迁移学习的算法的开发是机器学习社区持续关注的话题。 迁移学习对人类来说很常见,例如,我们可能会发现学习识别苹果可能有助于识别梨,或者学习弹奏电子琴可能有助于学习钢琴。

    找到目标问题的相似性,迁移学习任务就是从相似性出发,将旧领域(domain)学习过的模型应用在新领域上。

迁移学习的需求:

* 大数据与少标注的矛盾:虽然有大量的数据,但往往都是没有标注的,无法训练机器学习模型。人工进行数据标定太耗时。
* 大数据与弱计算的矛盾:普通人无法拥有庞大的数据量与计算资源。因此需要借助于模型的迁移。
* 普适化模型与个性化需求的矛盾:即使是在同一个任务上,一个模型也往往难以满足每个人的个性化需求,比如特定的隐私设置。这就需要在不同人之间做模型的适配。
* 特定应用(如冷启动)的需求。


数据集

    牛津大学的Oxford Visual Geometry Group(VGG),该小组隶属于1985年成立的Robotics Research Group,该Group研究范围包括了机器学习到移动机器人。就是他们设计了VGG网络,他们的网站上有各种各样的数据集,本文使用的是vgg网站上的宠物数据集

    数据集的统计如下:

dataset.jpg


Inception v3模型

    Inception 网络是 CNN 分类器发展史上一个重要的里程碑。在 Inception 出现之前,大部分流行 CNN 仅仅是把卷积层堆叠得越来越多,使网络越来越深,以此希望能够得到更好的性能。例如第一个得到广泛关注的 AlexNet,它本质上就是扩展 LeNet 的深度,并应用一些 ReLU、Dropout 等技巧。AlexNet 有 5 个卷积层和 3 个最大池化层,它可分为上下两个完全相同的分支,这两个分支在第三个卷积层和全连接层上可以相互交换信息。与 Inception 同年提出的优秀网络还有 VGG-Net,它相比于 AlexNet 有更小的卷积核和更深的层级。

    VGG-Net 的泛化性能非常好,常用于图像特征的抽取目标检测候选框生成等。VGG 最大的问题就在于参数数量,VGG-19 基本上是参数量最多的卷积网络架构。这一问题也是第一次提出 Inception 结构的 GoogLeNet 所重点关注的,它没有如同 VGG-Net 那样大量使用全连接网络,因此参数量非常小。

    GoogLeNet 最大的特点就是使用了 Inception 模块,它的目的是设计一种具有优良局部拓扑结构的网络,即对输入图像并行地执行多个卷积运算或池化操作,并将所有输出结果拼接为一个非常深的特征图。因为 1*1、3*3 或 5*5 等不同的卷积运算与池化操作可以获得输入图像的不同信息,并行处理这些运算并结合所有结果将获得更好的图像表征。

    另一方面,Inception 网络是复杂的(需要大量工程工作)。它使用大量 trick 来提升性能,包括速度和准确率两方面。它的不断进化带来了多种 Inception 网络版本的出现。常见的版本有:

Inception v1
Inception v2
Inception v3
Inception v4
Inception-ResNet

最原始的inception模块如下:

inception.jpg

    inception v3是在ImageNet上面训练的分类模型,因此可迁移到宠物分类任务。


测试Inception v3模型

    首先把inception模型下载、解压并加载到默认计算图,然后保存图结构用于tensorboard查看。代码如下:

import tensorflow as tf
import os
import tarfile
import requests

# inception模型下载地址
inception_pretrain_model_url = 'http://download.tensorflow.org/models/image/imagenet/inception-2015-12-05.tgz'
# 模型存放地址
inception_pretrain_model_dir = "inception_model"
if not os.path.exists(inception_pretrain_model_dir):
    os.makedirs(inception_pretrain_model_dir)

# 获取文件名,以及文件路径
filename = inception_pretrain_model_url.split('/')[-1]
filepath = os.path.join(inception_pretrain_model_dir, filename)
# 下载模型
if not os.path.exists(filepath):
    print("download: ", filename)
    r = requests.get(inception_pretrain_model_url, stream=True)
    with open(filepath, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            if chunk:
                f.write(chunk)
print("finish: ", filename)

# 解压文件
tarfile.open(filepath, 'r:gz').extractall(inception_pretrain_model_dir)
# 模型结构存放文件
log_dir = 'inception_log'
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

#读取模型
# classify_image_graph_def.pb为google训练好的模型
inception_graph_def_file = os.path.join(inception_pretrain_model_dir, 'classify_image_graph_def.pb')
with tf.Session() as sess:
    # 创建一个图来存放google训练好的模型
    with tf.gfile.FastGFile(inception_graph_def_file, 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
        tf.import_graph_def(graph_def, name='')
    # 保存图的结构
    writer = tf.summary.FileWriter(log_dir, sess.graph)
    writer.close()

在inception_log文件夹下打开tensorboard,可以看到inception的网络结构,如下:

t1.jpg

   

     inception模型在image net上训练的,那个是包含一千个分类的数据集。下载的模型压缩包解压也得到一些其它的文件,其中包含imagenet_2012_challenge_label_map_proto.pbtxt和imagenet_synset_to_human_label_map.txt这两个文件。这两个文件是模型label和图片类型id,字符串之间的映射,读取并得到字典的代码如下:

#读取inception的字典文件

id_to_class={}
id_to_str={}
class_to_str={}

with open('D:\Jupyter\TensorflowLearning\inception_model\imagenet_2012_challenge_label_map_proto.pbtxt','r') as f:
    lines=f.readlines()
    cls=0
    for line in lines:
        line=line.strip()
        if('target_class:' in line):
            cls=line[14:]
        if('target_class_string:' in line):
            id_to_class[line[-10:-1]]=cls
with open('D:\Jupyter\TensorflowLearning\inception_model\imagenet_synset_to_human_label_map.txt','r') as f:
    lines=f.readlines()
    for line in lines:
        line=line.strip()
        id_=line[:9]
        str_=line[10:]
        id_to_str[id_]=str_
        
for k in id_to_class.keys():
    class_to_str[id_to_class[k]]=id_to_str[k]
 
print(len(id_to_class))
print(len(id_to_str))
print(len(class_to_str)) #我们想要的


    接下来我们测试一下inception,首先读取加载模型到计算图:

import tensorflow as tf

#创建一个图存放inception模型
with tf.gfile.FastGFile('inception_model/classify_image_graph_def.pb', 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())
    tf.import_graph_def(graph_def, name='')

    接着准备一些狗的图片,丢进模型进行测试,需要说明的是,inception v3模型自带了jpeg图片预处理的节点,因此可以直接把图片丢进去。代码如下:

import os
import numpy as np
import re
from PIL import Image
import matplotlib.pyplot as plt

#使用模型
with tf.Session() as sess:
    softmax_tensor = sess.graph.get_tensor_by_name('softmax:0')
    # 遍历目录
    for root, dirs, files in os.walk('images/'):
        for file in files:
            image_data = tf.gfile.FastGFile(os.path.join(root, file), 'rb').read() #读入图片
            predictions = sess.run(softmax_tensor, {'DecodeJpeg/contents:0': image_data}) #喂入网络图片解码节点
            predictions = np.squeeze(predictions) #从数组的形状中删除单维度条目,即把shape中为1的维度去掉
            
            image_path = os.path.join(root, file)
            print(image_path)
            img = Image.open(image_path) #打开图片
            plt.imshow(img)#显示出来
            plt.axis('off')
            plt.show()
            #排序,然后倒序,最后取出最大的5个
            top_k = predictions.argsort()[-5:][::-1]
            
            for node_id in top_k:
                human_string = class_to_str[str(node_id)]
                score = predictions[node_id]
                print('%s (score=%.5f)' % (human_string, score))
            print()

测试结果如下:

test.jpg



微调模型

    谷歌提供了重新训练Inception v3模型的代码,得注意的是,github上面官方给的retrain.py需要翻墙下载inception模型,因此最好使用更早版本的retrain.py使用本地下载好的模型。这里有一个可用的retrain.py

    首先把前面提到的宠物数据集下载并解压。inception模型使用的文件名不能大小写混着,因此需要先把所有的图片名变小写,把'_'换成'vv'字母,然后根据文件名每个类归为一个文件夹。处理代码如下:

import os,shutil

base_dir='D:/CV/datasets/pets/images'

#对文件名预处理
for fileName in os.listdir(base_dir):#列出文件
    if(fileName[-3:]=='jpg'):#判断为jpg
        newName=fileName.lower()#变小写
        newName=newName.replace('_','vv')#替换vv
        src=os.path.join(base_dir,fileName)
        dst=os.path.join(base_dir,newName)
        os.rename(src, dst) 

for fileName in os.listdir(base_dir):#列出文件
    if(fileName[-3:]=='jpg'):#判断为jpg
        nameSplitList=fileName.split('vv')
        nameSplitList.pop()
        clsName='vv'.join(nameSplitList) #根据文件名将文件移到对应文件夹
        clsDirName=os.path.join(base_dir,clsName)
        if(not os.path.exists(clsDirName)):
            os.mkdir(clsDirName)
        fliePath=os.path.join(base_dir,fileName)
        shutil.move(fliePath,clsDirName)
print('done')

    接下来就可以调用retrain.py重新训练下载了的inception模型了。我写了一个批处理文件如下:

E:/Anaconda/envs/ai/python retrain.py ^    
--bottleneck_dir bottleneck_dir ^  
--how_many_training_steps 1000 ^   
--model_dir D:/Jupyter/TensorflowLearning/inception_model ^   
--output_graph output_graph.pb ^   
--output_labels output_labels.txt ^   
--image_dir D:/CV/datasets/pets  
pause

代码解释如下:

执行retrain.py  
E:/Anaconda/envs/ai/python retrain.py ^  
存放bottleneck输出的高级特征的路径
--bottleneck_dir bottleneck_dir ^  
训练步数  
--how_many_training_steps 1000 ^  
未解压的模型文件路径  
--model_dir D:/Jupyter/TensorflowLearning/inception_model ^  
输出训练好的模型路径  
--output_graph output_graph.pb ^  
输出的标签路径
--output_labels output_labels.txt ^  
训练集路径  
--image_dir D:/CV/datasets/pets/images  
pause

    执行之后便开始训练。

    首先代码会检查数据集目录下有哪些类别:

1.jpg

    然后每个训练样本生成一个bottleneck输出的特征值,并保存至txt文件:

2.jpg

    接着使用这些特征值对输出层进行训练:

3.jpg

    最后得到测试精度为94%,还算不错。

4.jpg


    训练过程中会在本磁盘根目录下生成一个tmp文件夹,里面包含了训练过程中的参数,使用tensorboard查看如下:

acc.jpg

    新的模型架构,其中就是在原来inception的bottleneck位置新加了一个输出层:

model.jpg


测试迁移模型

    我找了猫狗分类的数据集来测试了一下,代码如下:

dict_cls2str={}
with open('output_labels.txt','r') as f:
    lines=f.readlines()
    for i,line in enumerate(lines):
        line=line.strip()
        line=line.replace('vv',' ')
        dict_cls2str[i]=line
print(dict_cls2str)

读取并测试模型:

%matplotlib inline
import tensorflow as tf
import numpy as np
import cv2
import matplotlib.pyplot as plt
import os
from PIL import Image

test_dir='D:/CV/datasets/pets/test'
#读入模型
with tf.gfile.FastGFile('output_graph.pb','rb') as f:
    graph_def=tf.GraphDef()
    graph_def.ParseFromString(f.read())
    tf.import_graph_def(graph_def,name='')
    
with tf.Session() as sess:
    #获得我们训练的输出张量
    softmax_tensor=sess.graph.get_tensor_by_name('final_result:0')
    for fileName in os.listdir(test_dir):
        pic_path=os.path.join(test_dir,fileName)
        #载入图片
        img=tf.gfile.FastGFile(pic_path,'rb').read()
        #将图片传入计算图
        pred=sess.run(softmax_tensor,{'DecodeJpeg/contents:0':img})
        pred=np.squeeze(pred)
        
        print(pic_path)
        #读取图片
        img=Image.open(pic_path)
        #显示图片
        plt.imshow(img)
        plt.axis('off')
        plt.show()
        
        #排序,然后倒序
        top_k=pred.argsort()[::-1]
        for cls in top_k[:5]:
            name=dict_cls2str[cls]
            print('%s %f'%(name,pred[cls]))

测试的效果如下:

test_ew.jpg

可以说还是非常不错的。



参考文献

[1]mantch.迁移学习(Transfer),面试看这些就够了!(附代码).http://blog.itpub.net/69942346/viewspace-2654034/  .2019-08-18

[2]机器之心.一文概览Inception家族的「奋斗史」.http://baijiahao.baidu.com/s?id=1601882944953788623&wfr=spider&for=pc  .18-05-30

[3]Christian Szegedy,Wei Liu,etc.Going deeper with convolutions

首页 所有文章 机器人 计算机视觉 自然语言处理 机器学习 编程随笔 关于