前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >迁移学习(Transfer Learning)

迁移学习(Transfer Learning)

作者头像
CristianoC
发布2020-06-02 11:15:26
1.3K0
发布2020-06-02 11:15:26
举报

前言

距离上次更公众号已经有一段时间了,寒假到开学这段时间都没有更新,笔者在这跟大家说声抱歉。这个学期可能会更新一些有关深度学习的文章,尽量保持一周一更,也希望大家监督。话不多说,开始正题。

目录

1.迁移学习的概念

2.为什么要迁移学习

3.迁移学习的分类

4.迁移学习的方法

5.关于迁移学习的思考和优化

6.基于VGG关于迁移学习的一个实例

迁移学习的概念

迁移学习是属于机器学习的一种研究领域。它专注于存储已有问题的解决模型,并将其利用在其他不同但相关问题上,正如人类可以将一个领域学习到的知识和经验,应用到其他相似的领域中去一样,机器同样也能做到。

为什么要迁移学习

传统的机器学习/数据挖掘只有在训练集数据和测试集数据都来自同一个feature space(特征空间)和统一分布的时候才运行的比较好,这意味着每一次换了数据都要重新训练模型,太麻烦了。比如:

(1)从数据类型/内容上看,对于新的数据集,获取新的训练数据很贵也很难。

(2)从时间维度上看,有些数据集很容易过期,即不同时期的数据分布也会不同。

迁移学习的分类

在讲分类之前,我先介绍两个概念,方便等会读者的理解。

domain(域)和task(任务),source(源)和target(目标),然后给它们进行自由组合。

domain:包括两部分:1.feature space(特征空间);2.probability(概率)。所以当我们说domain不同的时候,就得分两种情况。可能是feature space不同,也可能是feature space一样但probability不同。

task:包括两部分:1. label space(标记空间);2.objective predictive function(目标预测函数)。同理,当我们说task不同的时候,就得分两种情况。可能是label space不同,也可能是label space一样但function不同。

source和target就不用说了,前者是用于训练模型的域/任务,后者是要用前者的模型对自己的数据进行预测/分类/聚类等机器学习任务的域/任务。

一、从迁移的内容来分类。

(1)Instance-based TL(样本迁移)

尽管source domain数据不可以整个直接被用到target domain里,但是在source domain中还是找到一些可以重新被用到target domain中的数据。对它们调整权重,使它能与target domain中的数据匹配之后可以进行迁移。盗一张图,比如在这个例子中就是找到例子3,然后加重它的权值,这样在预测的时候它所占权重较大,预测也可以更准确。

instance reweighting(样本重新调整权重)和importance sampling(重要性采样)是instance-based TL里主要用到的两项技术。

(2)Feature-representation-transfer(特征迁移)

找到一些好的有代表性的特征,通过特征变换把source domain和target domain的特征变换到同样的空间,使得这个空间中source domain和target domain的数据具有相同的分布,然后进行传统的机器学习就可以了。

(3)Parameter-transfer(参数/模型迁移)

假设source tasks和target tasks之间共享一些参数,或者共享模型hyperparameters(超参数)的先验分布。这样把原来的模型迁移到新的domain时,也可以达到不错的精度。

(4)Relational-knowledge-transfer(关系迁移)

把相似的关系进行迁移,比如生物病毒传播到计算机病毒传播的迁移,比如师生关系到上司下属关系的迁移。

二、从迁移的场景来分类

(1)Inductive TL(归纳式迁移学习)

source和target的domain可能一样或不一样,task不一样;target domain的labeled数据可得,source domain不一定可得。所以呢,根据source domain的labeled数据可以再细分为两类:

multitask learning(多任务学习):source domain的labeled数据可得。

self-taught learning(自学习):source domain的labeled数据不可得。

(2)Transductive TL(直推式迁移学习)

source和target的task一样,domain不一样;source domain的labeled数据可得,target domain的不可得。注意我们提过,domain不一样意味着两种可能:feature space不一样,或者feature space一样而probability不一样。而后一种情况和domain adaptation(域适配)息息相关。这里也可以根据domain和task的个数分为两个情况:

Domain Adaptation(域适配):不同的domains+single task

Sample Selection Bias(样本选择偏差)/Covariance Shift(协方差转变):single domain+single task

(3)Unsupervised TL(无监督迁移学习)

source和target的domain和task都不一样;source domain和target domain的labeled数据都不可得。

综上,这几个方法差别主要是:(1)source和domain之间,domain是否相同,task是否相同;(2)source domain和target domain的labeled数据是否可以得到。

迁移学习的方法

迁移学习的方法有许多,通过source target domain task不同情况的组合都有不同的方法,在这里我就简述一下一般迁移学习的方法流程。

我们知道在做深度网络的时候,一开始网络学的是general(一般)的特征,之后才越来越细化,越来越specific(具体)。那么到底怎么衡量一层是general和specific的呢?这种转变到底是突然在某一层发生的,还是慢慢渐变式地发生的呢?这种转变是在哪个部分发生的,开始、中间、还是最后一层?研究这些问题,是因为这些问题对研究迁移效果很有帮助,因为我们进行迁移,本质就是要找出source和domain里的共同点,所以要在general层面上进行迁移。因此,找出哪一层是general的,哪一层是specific的,也就显得至关重要了。

一般的迁移学习是这样的:训练好一个网络(我们称它为base network)→把它的前n层复制到target network的前n层→target network剩下的其他层随机初始化→开始训练target task.。其中,在做backpropogate(反向传播)的时候,有两种方法可以选择:

(1)把迁移过来的这前n层frozen(冻结)起来,即在训练target task的时候,不改变这n层的值;

(2)不冻结这前n层,而是会不断调整它们的值,称为fine-tune(微调)。这个主要取决于target数据集的大小和前n层的参数个数,如果target数据集很小,而参数个数很多,为了防止overfitting(过拟合),通常采用frozen方法;反之,采用fine-tune。

关于迁移学习的思考和优化

我们假设:

A和B:两个task,可以分割为两个相似的数据集(random A/B splits)和不相似的数据集(man-made and natural)

设定:ImageNet分类类别为1000个,可以分割为两个分别有500个类别的数据集,也可以分割为分别有645000个样本的数据集。

层数总共为8层,为了更好地举例子,我们对上面所说的“前n层”里面的n取n=3,把这个第n层称为example layer。

base A:A上训练的神经网络

base B:B上训练的神经网络

transfer A3B:前3层从base A里面的前3层得到,且前3层是frozen的;后5层随机初始化,且在B上进行训练。重点来了,如果A3B和baseB的效果差不多,说明第3层对B来说仍然是general的;如果差多了,那第3层就是specific to A的,不适合迁移到B;

selffer B3B+:和B3B一样,只不过它不用frozen前3层,而是会学习所有层数,即它是fine tune的;

transfer A3B+:和A3B一样,只不过它不用frozen前3层,而是会学习所有层数,即它是fine tune的。

用一张图来展示:

思考优化:

一、深度网络里不同层次迁移效果,展示了不使用fine-tune时,迁移效果下降的原因可能有两个:

(1)特征本身specificity,就比如现在已经到了网络稍深一点的层次了,网络学的已经是specific的特征,这时候去迁移效果会不好。解决方法就是选出general的层,进行迁移;

(2)一个特征之间是co-adapted(耦合)的网络分割了。这一个我的理解可能是和frozen相关,就是本来网络里的特征是耦合的、紧密联系的,但是因为我们把前n层frozen了,相当于把网络割成了两部分,这样可能会导致效果不好。解决方法就是fine tune。

二、相似数据集之间的迁移效果优于不同数据集之间的迁移效果。

三、训练网络时,使用迁移的weights(权重)去初始化的效果会比随机初始化的效果要好,无论是在相似的数据集上迁移还是在不相似的数据集上迁移。

四、无论使用多少层的迁移特征对网络进行初始化,在fine tune之后效果都会变得很好。

一个以VGG为背景的迁移学习的例子

我先稍微介绍一下VGG:VGG 是视觉领域竞赛 ILSVRC 在 2014 年的获胜模型,以 7.3% 的错误率在 ImageNet 数据集上大幅刷新了前一年 11.7% 的世界纪录。VGG16 基本上继承了 AlexNet 深的思想,并且发扬光大,做到了更深。AlexNet 只用到了 8 层网络,而 VGG 的两个版本分别是 16 层网络版和 19 层网络版。在接下来的例子中,我会采用稍微简单的一些的 VGG16,他和 VGG19 有几乎完全一样的准确度,但是运算起来更快一些。

VGG16在1000个类别中训练过,我提取了VGG前面的卷积尺化层,重新组建了后面的全连接层,让它做一些和原来不太相干的事情。我从网上下载了将近1000张猫和老虎的照片,然后伪造了一些猫和老虎长度的数据,最后让迁移后的网络分辨出猫和老虎的长度。

猫和老虎照片如下:

猫和老虎体长的数据:

另外我们还要下载一个VGG16的模型,是一个.npy文件,是一个numpy对象,笔者是上github下载的。

准备好数据,我们就可以开始进行迁移VGG了。

我保留了VGG前面的卷积尺化层,只是把后面的全连接层给拆了,改成可被train的两层,输出一个数字,这个数字代表这只猫或者老虎的长度。(也就是采取冻结而非微调的方式)

ps:代码有部分涉及到VGG16源码,在此不作过多解读,有空专门写一篇读VGG代码的文章。

代码语言:javascript
复制
class Vgg16:
……

        conv5_1 = self.conv_layer(pool4, "conv5_1")
        conv5_2 = self.conv_layer(conv5_1, "conv5_2")
        conv5_3 = self.conv_layer(conv5_2, "conv5_3")
        pool5 = self.max_pool(conv5_3, 'pool5')

        # detach original VGG fc layers and
        # reconstruct your own fc layers serve for your own purpose
        self.flatten = tf.reshape(pool5, [-1, 7*7*512])
        self.fc6 = tf.layers.dense(self.flatten, 256, tf.nn.relu, name='fc6')
        self.out = tf.layers.dense(self.fc6, 1, name='out')

        self.sess = tf.Session()
        if restore_from:
            saver = tf.train.Saver()
            saver.restore(self.sess, restore_from)
        else:   # training graph
            self.loss = tf.losses.mean_squared_error(labels=self.tfy, predictions=self.out)
            self.train_op = tf.train.RMSPropOptimizer(0.001).minimize(self.loss)
            self.sess.run(tf.global_variables_initializer())

    def max_pool(self, bottom, name):
        return tf.nn.max_pool(bottom, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name=name)

    def conv_layer(self, bottom, name):
        with tf.variable_scope(name):   # CNN's filter is constant, NOT Variable that can be trained(前面几层都是从文件读取,无法被训练)
            conv = tf.nn.conv2d(bottom, self.data_dict[name][0], [1, 1, 1, 1], padding='SAME')
            lout = tf.nn.relu(tf.nn.bias_add(conv, self.data_dict[name][1]))
            return lout

在 self.flatten 之前的 layers, 都是不能被 train 的. 而 tf.layers.dense() 建立的 layers 是可以被 train 的. 到时候我们 train 好了, 再定义一个 Saver 来保存由 tf.layers.dense() 建立的 parameters.

代码语言:javascript
复制
 def save(self, path='./for_transfer_learning/model/transfer_learn'):
        saver = tf.train.Saver()
        saver.save(self.sess, path, write_meta_graph=False)

接着就可以开始训练了

因为我们有了训练好的VGG16,我们就可以把VGG16的卷积层想象成一个feature extractor来提取或压缩图片中的特征。这其实和 Autoencoder 中的 encoder 类似,用这些提取的特征来训练后面的我们自己编写的全连接层。因为我这里采取的是冻结的方式,也就是只需要训练自己编写的全连接层,所以我只训练了100次,如果你采取微调的方式,那训练100次是远远不够的。

代码语言:javascript
复制
def train():
……
    vgg = Vgg16(vgg16_npy_path='./for_transfer_learning/vgg16.npy')
    print('Net built')
    for i in range(100):
        b_idx = np.random.randint(0, len(xs), 6)
        train_loss = vgg.train(xs[b_idx], ys[b_idx])
        print(i, 'train loss: ', train_loss)

    vgg.save('./for_transfer_learning/model/transfer_learn') 

训练好之后我们就可以开始测试了,我输入了一张猫,一张老虎的图,训练好的网络给了我他的答案:

这样一个小的迁移学习的例子就完成了,最后附上全部代码

代码语言:javascript
复制
from urllib.request import urlretrieve
import os
import numpy as np
import tensorflow as tf
import skimage.io
import skimage.transform
import matplotlib.pyplot as plt
def load_img(path):
    img = skimage.io.imread(path)
    img = img / 255.0
    # print "Original Image Shape: ", img.shape
    # we crop image from center
    short_edge = min(img.shape[:2])
    yy = int((img.shape[0] - short_edge) / 2)
    xx = int((img.shape[1] - short_edge) / 2)
    crop_img = img[yy: yy + short_edge, xx: xx + short_edge]
    # resize to 224, 224
    resized_img = skimage.transform.resize(crop_img, (224, 224))[None, :, :, :]   # shape [1, 224, 224, 3]
    return resized_img

def load_data():
    imgs = {'tiger': [], 'kittycat': []}
    for k in imgs.keys():
        dir = './for_transfer_learning/data/' + k
        for file in os.listdir(dir):
            if not file.lower().endswith('.jpg'):
                continue
            try:
                resized_img = load_img(os.path.join(dir, file))
            except OSError:
                continue
            imgs[k].append(resized_img)    # [1, height, width, depth] * n
            if len(imgs[k]) == 400:        # only use 400 imgs to reduce my memory load
                break
    # fake length data for tiger and cat
    tigers_y = np.maximum(20, np.random.randn(len(imgs['tiger']), 1) * 30 + 100)
    cat_y = np.maximum(10, np.random.randn(len(imgs['kittycat']), 1) * 8 + 40)
    return imgs['tiger'], imgs['kittycat'], tigers_y, cat_y

class Vgg16:
    vgg_mean = [103.939, 116.779, 123.68]

    def __init__(self, vgg16_npy_path=None, restore_from=None):
        # pre-trained parameters
        try:
            self.data_dict = np.load(vgg16_npy_path, encoding='latin1').item()#遍历其内键值对,导入模型参数
        except FileNotFoundError:
            print('Please download VGG16 parameters from here https://mega.nz/#!YU1FWJrA!O1ywiCS2IiOlUCtCpI6HTJOMrneN-Qdv3ywQP5poecM\nOr from my Baidu Cloud: https://pan.baidu.com/s/1Spps1Wy0bvrQHH2IMkRfpg')

        self.tfx = tf.placeholder(tf.float32, [None, 224, 224, 3])
        self.tfy = tf.placeholder(tf.float32, [None, 1])

        # Convert RGB to BGR
        red, green, blue = tf.split(axis=3, num_or_size_splits=3, value=self.tfx * 255.0)
        bgr = tf.concat(axis=3, values=[
            blue - self.vgg_mean[0],
            green - self.vgg_mean[1],
            red - self.vgg_mean[2],
        ])# 逐样本减去每个通道的像素平均值,这种操作可以移除图像的平均亮度值,该方法常用在灰度图像上

        # pre-trained VGG layers are fixed in fine-tune
        conv1_1 = self.conv_layer(bgr, "conv1_1")
        conv1_2 = self.conv_layer(conv1_1, "conv1_2")
        pool1 = self.max_pool(conv1_2, 'pool1')

        conv2_1 = self.conv_layer(pool1, "conv2_1")
        conv2_2 = self.conv_layer(conv2_1, "conv2_2")
        pool2 = self.max_pool(conv2_2, 'pool2')

        conv3_1 = self.conv_layer(pool2, "conv3_1")
        conv3_2 = self.conv_layer(conv3_1, "conv3_2")
        conv3_3 = self.conv_layer(conv3_2, "conv3_3")
        pool3 = self.max_pool(conv3_3, 'pool3')

        conv4_1 = self.conv_layer(pool3, "conv4_1")
        conv4_2 = self.conv_layer(conv4_1, "conv4_2")
        conv4_3 = self.conv_layer(conv4_2, "conv4_3")
        pool4 = self.max_pool(conv4_3, 'pool4')

        conv5_1 = self.conv_layer(pool4, "conv5_1")
        conv5_2 = self.conv_layer(conv5_1, "conv5_2")
        conv5_3 = self.conv_layer(conv5_2, "conv5_3")
        pool5 = self.max_pool(conv5_3, 'pool5')

        # detach original VGG fc layers and
        # reconstruct your own fc layers serve for your own purpose
        self.flatten = tf.reshape(pool5, [-1, 7*7*512])
        self.fc6 = tf.layers.dense(self.flatten, 256, tf.nn.relu, name='fc6')
        self.out = tf.layers.dense(self.fc6, 1, name='out')

        self.sess = tf.Session()
        if restore_from:
            saver = tf.train.Saver()
            saver.restore(self.sess, restore_from)
        else:   # training graph
            self.loss = tf.losses.mean_squared_error(labels=self.tfy, predictions=self.out)
            self.train_op = tf.train.RMSPropOptimizer(0.001).minimize(self.loss)
            self.sess.run(tf.global_variables_initializer())

    def max_pool(self, bottom, name):
        return tf.nn.max_pool(bottom, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name=name)

    def conv_layer(self, bottom, name):
        with tf.variable_scope(name):   # CNN's filter is constant, NOT Variable that can be trained(前面几层都是从文件读取,无法被训练)
            conv = tf.nn.conv2d(bottom, self.data_dict[name][0], [1, 1, 1, 1], padding='SAME')
            lout = tf.nn.relu(tf.nn.bias_add(conv, self.data_dict[name][1]))
            return lout

    def train(self, x, y):
        loss, _ = self.sess.run([self.loss, self.train_op], {self.tfx: x, self.tfy: y})
        return loss

    def predict(self, paths):
        fig, axs = plt.subplots(1, 2)
        for i, path in enumerate(paths):
            x = load_img(path)
            length = self.sess.run(self.out, {self.tfx: x})
            axs[i].imshow(x[0])
            axs[i].set_title('Len: %.1f cm' % length)
            axs[i].set_xticks(()); axs[i].set_yticks(())
        plt.show()

    def save(self, path='./for_transfer_learning/model/transfer_learn'):
        saver = tf.train.Saver()
        saver.save(self.sess, path, write_meta_graph=False)

def train():
    tigers_x, cats_x, tigers_y, cats_y = load_data()

    # plot fake length distribution
    plt.hist(tigers_y, bins=20, label='Tigers')
    plt.hist(cats_y, bins=10, label='Cats')
    plt.legend()
    plt.xlabel('length')
    plt.show()

    xs = np.concatenate(tigers_x + cats_x, axis=0)
    ys = np.concatenate((tigers_y, cats_y), axis=0)

    vgg = Vgg16(vgg16_npy_path='./for_transfer_learning/vgg16.npy')
    print('Net built')
    for i in range(100):
        b_idx = np.random.randint(0, len(xs), 6)
        train_loss = vgg.train(xs[b_idx], ys[b_idx])
        print(i, 'train loss: ', train_loss)

    vgg.save('./for_transfer_learning/model/transfer_learn')      # save learned fc layers


def eval():
    vgg = Vgg16(vgg16_npy_path='./for_transfer_learning/vgg16.npy',
                restore_from='./for_transfer_learning/model/transfer_learn')
    vgg.predict(
        ['./for_transfer_learning/data/kittycat/000129037.jpg', './for_transfer_learning/data/tiger/391412.jpg'])


if __name__ == '__main__':
    # download()
     #train()
    eval()

参考:

1.https://ieeexplore.ieee.org/abstract/document/5288526/

2.http://yosinski.com/media/papers/Yosinski__2014__NIPS__How_Transferable_with_Supp.pdf

3.https://me.csdn.net/vvnzhang2095

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-03-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 计算机视觉漫谈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档