迁移学习(Transfer Learning)

2020-06-02 11:15:26 浏览数 (1)

前言

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

目录

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-Qdv3ywQP5poecMnOr 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

0 人点赞