预训练模型与迁移学习

一、什么是迁移学习

聪明人都喜欢"偷懒",因为这样的偷懒能帮我们节省大量的时间提高效率。有一种偷懒是 "站在巨人的肩膀上",也就是表示要善于学习先辈的经验。这句话放在机器学习中就是指的迁移学习。
迁移学习是一种机器学习技术,顾名思义就是指将知识从一个领域迁移到另一个领域的能力。
我们知道,神经网络需要用数据来训练,它从数据中获得信息,进而把它们转换成相应的权重。这些权重能够被提取出来,迁移到其他的神经网络中,我们"迁移"了这些学来的特征,就不需要从零开始训练一个神经网络了 。

迁移学习的价值
复用现有知识域数据,已有的大量工作不至于完全丢弃;
不需要再去花费巨大代价去重新采集和标定庞大的新数据集,也有可能数据根本无法获取;
对于快速出现的新领域,能够快速迁移和应用,体现时效性优势。

二、迁移学习的载体:预训练模型

在计算机视觉领域中,迁移学习通常是通过使用预训练模型来体现的。预训练模型是在大型基准数据集上训练的模型,用于解决相似的问题。由于训练这种模型的计算成本较高,因此,导入已发布的成果并使用相应的模型是比较常见的做法。

1、keras.Application

Kera的应用模块Application提供了带有预训练权重的Keras模型,这些模型可以用来进行预测、特征提取和finetune。
目前,Keras 包含有 5 个预训练模型,分别为:VGG16,VGG19,ResNet50,InceptionV3,Xception,MobileNet
(1)VGG16/ VGG19
Keras 导入 VGG16 和 VGG19 模型及默认参数如下:

from keras.applications import vgg16
from keras.applications import vgg19
vgg16.VGG16(include_top=True, weights='imagenet', input_tensor=None, input_shape=None, pooling=None, classes=1000)
vgg19.VGG19(include_top=True, weights='imagenet', input_tensor=None, input_shape=None, pooling=None, classes=1000)

(2)ResNet50
Keras 导入 ResNet50 模型及默认参数如下:

from keras.applications import resnet50
resnet50.ResNet50(include_top=True, weights='imagenet', input_tensor=None, input_shape=None, pooling=None, classes=1000)

(3)InceptionV3
Keras 导入 InceptionV3 模型及默认参数如下:

from keras.applications import inception_v3
inception_v3.InceptionV3(include_top=True, weights='imagenet', input_tensor=None, input_shape=None, pooling=None, classes=1000)

(4)Xception
Keras 导入 Xception 模型及默认参数如下

from keras.applications import xception
xception.Xception(include_top=True, weights='imagenet', input_tensor=None, input_shape=None, pooling=None, classes=1000)

(5)MobileNet
Keras 导入 MobileNet 模型及默认参数如下:

from keras.applications import mobilenet
mobilenet.MobileNet(input_shape=None, alpha=1.0, depth_multiplier=1, dropout=1e-3, include_top=True, weights='imagenet', input_tensor=None, pooling=None, classes=1000)

举例:使用预训练模型输出图像分类预测

import numpy as np
from keras.preprocessing import image
from keras.applications.inception_v3 import InceptionV3
from keras.applications.inception_v3 import preprocess_input
from keras.applications.inception_v3 import decode_predictions

# 新建模型,此处实际上是导入预训练模型
model = InceptionV3()
model.summary()

# 按照 InceptionV3 模型的默认输入尺寸,载入 demo1 图像
img = image.load_img('demo1.jpg', target_size=(299, 299))

# 提取特征
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)

# 预测并输出概率最高的三个类别
preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])

官方文档

2、自己下载预训练权重

VGG16:
WEIGHTS_PATH = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg16_weights_tf_dim_ordering_tf_kernels.h5
WEIGHTS_PATH_NO_TOP = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5
VGG19:
WEIGHTS_PATH = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.4/xception_weights_tf_dim_ordering_tf_kernels.h5
WEIGHTS_PATH_NO_TOP = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.4/xception_weights_tf_dim_ordering_tf_kernels_notop.h5
RESNET50:
WEIGHTS_PATH = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels.h5
WEIGHTS_PATH_NO_TOP = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5
INCEPTIONS_V3:
WEIGHTS_PATH = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels.h5
WEIGHTS_PATH_NO_TOP = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.5/inception_v3_weights_tf_dim_ordering_tf_kernels_notop.h5
XCEPTION:
WEIGHTS_PATH = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.4/xception_weights_tf_dim_ordering_tf_kernels.h5
WEIGHTS_PATH_NO_TOP = ‘https://github.com/fchollet/deep-learning-models/releases/download/v0.4/xception_weights_tf_dim_ordering_tf_kernels_notop.h5

三、代码实现

现在应用迁移学习来实现一个特定的图像分类任务

# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
import os, shutil
FILE_DIR = os.path.dirname(os.path.abspath(__file__))
from tqdm import tqdm
from sklearn.datasets import load_files
# 载入画图所需要的库 matplotlib
import matplotlib.pyplot as plt 
# 导入karas神经网络框架
import keras
from keras.optimizers import Adam
from keras.models import Model
from keras.utils import np_utils,plot_model
from keras.preprocessing import image
from keras.applications import inception_v3
from keras.layers import Conv2D, GlobalAveragePooling2D, Activation, Dropout, Dense
from keras.callbacks import ModelCheckpoint,EarlyStopping,ReduceLROnPlateau,TensorBoard

#========================================================
#  全局参数
#========================================================

# 训练参数
num_epochs = 10
batch_size = 32

# 模型参数存储
weight_url = 'D://saved_models/V3.hdf5'
best_weight_url = 'D://saved_models/V3_best.hdf5'

#========================================================
#  文件准备
#========================================================

def image_preparation(original_dir, base_dir, labels):
    '''
    图像分类文件准备, 将文件复制到训练\验证\测试集目录
    INPUT  -> 原始数据集地址, 数据集存放地址, 分类列表
    '''
    # 定义文件地址
    train_dir = os.path.join(base_dir, 'train')
    if not os.path.exists(train_dir):
        os.mkdir(train_dir)
    validation_dir = os.path.join(base_dir, 'validation')
    if not os.path.exists(validation_dir):
        os.mkdir(validation_dir)
    test_dir = os.path.join(base_dir, 'test')
    if not os.path.exists(test_dir):
        os.mkdir(test_dir)
    
    names = locals()
    # 图片迁移
    for label in labels:
        names["train_"+str(label)+"dir"] =  os.path.join(train_dir, str(label))
        if not os.path.exists(names["train_"+str(label)+"dir"]):
            os.mkdir(names["train_"+str(label)+"dir"])
        names["validation_"+str(label)+"dir"] =  os.path.join(validation_dir, str(label))
        if not os.path.exists(names["validation_"+str(label)+"dir"]):
            os.mkdir(names["validation_"+str(label)+"dir"])
        names["test_"+str(label)+"dir"] =  os.path.join(test_dir, str(label))
        if not os.path.exists(names["test_"+str(label)+"dir"]):
            os.mkdir(names["test_"+str(label)+"dir"])

        fnames = [str(label)+'.{}.jpg'.format(i) for i in range(1000)]
        for fname in fnames:
            src = os.path.join(original_dir, fname)
            dst = os.path.join(names["train_"+str(label)+"dir"], fname)
            shutil.copyfile(src, dst)
        fnames = [str(label)+'.{}.jpg'.format(i) for i in range(1000, 1500)]
        for fname in fnames:
            src = os.path.join(original_dir, fname)
            dst = os.path.join(names["validation_"+str(label)+"dir"], fname)
            shutil.copyfile(src, dst)
        fnames = [str(label)+'.{}.jpg'.format(i) for i in range(1500, 2000)]
        for fname in fnames:
            src = os.path.join(original_dir, fname)
            dst = os.path.join(names["test_"+str(label)+"dir"], fname)
            shutil.copyfile(src, dst)
        print('total train '+str(label)+' images:', len(os.listdir(names["train_"+str(label)+"dir"])))
        print('total validation '+str(label)+' images:', len(os.listdir(names["validation_"+str(label)+"dir"])))
        print('total test '+str(label)+' images:', len(os.listdir(names["test_"+str(label)+"dir"])))

# 将数据分别存到各个文件夹
originial_dataset_dir = 'D:\download\kaggle_original_data'
base_dir = 'D:\cats_and_dogs'
if not os.path.exists(base_dir):
    os.mkdir(base_dir)
image_preparation(originial_dataset_dir, base_dir, ['cat','dog'])

# 分类数
n_classes = 0
for fn in os.listdir(os.path.join(base_dir, 'train')):
    n_classes += 1
#========================================================
#  图像加载
#========================================================
def load_dataset(path):
    data = load_files(path)
    data_files = np.array(data['filenames'])
    data_targets = np_utils.to_categorical(np.array(data['target']), n_classes)
    return data_files, data_targets

train_files, train_targets = load_dataset(os.path.join(base_dir, 'train'))
valid_files, valid_targets = load_dataset(os.path.join(base_dir, 'validation'))

#========================================================
#  图像预处理
#========================================================
def path_to_tensor(img_path):
    '''单个图片格式处理'''
    img = image.load_img(img_path, target_size=(299, 299))
    x = image.img_to_array(img)
    # 将3维张量转化为格式为(1, 299, 299, 3)的4维张量并进行归一化到0-1
    return np.expand_dims(x, axis=0).astype('float32')/255.0

def paths_to_tensor(img_paths):
    '''批量图片格式处理'''
    list_of_tensors = [path_to_tensor(img_path) for img_path in tqdm(img_paths)]
    return np.vstack(list_of_tensors)

train_tensors = paths_to_tensor(train_files)
valid_tensors = paths_to_tensor(valid_files)

#========================================================
#  模型声明
#========================================================
def InceptionV3_model(lr=0.005):
    '''构造基于InceptionV3的迁移学习模型'''
    base_model = inception_v3.InceptionV3(weights='imagenet', include_top=False)

    # 冻结base_model所有层,这样就可以正确获得bottleneck特征
    for layer in base_model.layers:
        layer.trainable = False

    x = base_model.output
    # 重新配置全连接层,添加自己的全链接分类层
    x = GlobalAveragePooling2D(name='average_pooling2d_new')(x)
    x = Dense(1024, activation='relu', name='dense_new')(x)
    predictions = Dense(n_classes, activation='softmax', name='dense_output')(x)
    # 创建最终模型
    model = Model(inputs=base_model.input, outputs=predictions)

    # 模型编译
    adam = Adam(lr=lr, beta_1=0.9, beta_2=0.999, epsilon=1e-8)
    model.compile(loss='categorical_crossentropy', 
                  optimizer=adam, 
                  metrics=['accuracy'])
    
    # model.summary()
    # plot_model(model, to_file='V3_model.png')
    return model

# 实例化模型
V3_model = InceptionV3_model()
#========================================================
#  模型训练
#========================================================
def train(X, Y, X_val, Y_val, model):
    # 载入已保存的权重, 继续训练
    if os.path.exists(weight_url):
        model.load_weights(weight_url)

    # 训练过程中的回调函数(检查点\早期停止\动态学习率\训练日志)
    Checkpoint = ModelCheckpoint(filepath=best_weight_url,
                                 save_best_only=True,
                                 verbose=1)
    EarlyStop = EarlyStopping(monitor='val_loss',
                              patience=5,
                              mode='auto',
                              verbose=1)
    lrate = ReduceLROnPlateau(monitor='val_loss',  
                              factor=0.1,  # 每次减少学习率的因子,学习率将以lr = lr*factor的形式被减少 
                              patience=3,  # 当patience个epoch过去而模型性能不提升时,学习率减少的动作会被触发
                              mode='auto', 
                              min_delta=0.0001, # 阈值,用来确定是否进入检测值的“平原区” 
                              cooldown=0, # 学习率减少后,会经过cooldown个epoch才重新进行正常操作
                              min_lr=0,  # 学习率的下限 
                              verbose=1)
    tb = TensorBoard(log_dir=FILE_DIR,  # log 目录
                     histogram_freq=1,  # 按照何等频率(epoch)来计算直方图,0为不计算
                     batch_size=batch_size,  # 用多大量的数据计算直方图
                     write_graph=True,       # 是否存储网络结构图
                     write_grads=False,      # 是否可视化梯度直方图
                     write_images=False,     # 是否可视化参数
                     embeddings_freq=0,
                     embeddings_layer_names=None,
                     embeddings_metadata=None)
                                
    history_ft = model.fit(X, Y,
                           validation_data = (X_val, Y_val),
                           # validation_split = 0.2,
                           epochs=num_epochs,
                           batch_size=batch_size,
                           # steps_per_epoch=None, # steps_per_epoch=10,则就是将一个epoch分为10份,不能和batch_size共同使用
                           # validation_steps=None, # 当steps_per_epoch被启用的时候才有用,验证集的batch_size
                           callbacks=[Checkpoint, EarlyStop, lrate, tb], 
                           verbose=1
                           )

    # 参数保存,留待下次继续训练
    model.save_weights(weight_url, overwrite=True)
    return history_ft

def plot_training(data):
    '''绘制模型正确率曲线和损失曲线'''
    acc = history.history['acc']
    val_acc = history.history['val_acc']
    loss = history.history['loss']
    val_loss = history.history['val_loss']
    # 正确率曲线
    plt.figure()
    plt.title('Train and valid accuracy')
    plt.plot(data.epoch,acc,label="train_acc")
    plt.plot(data.epoch,val_acc,label="val_acc")
    plt.scatter(data.epoch,data.history['acc'],marker='*')
    plt.scatter(data.epoch,data.history['val_acc'],marker='*')
    plt.legend()
    plt.show()
    # 损失曲线
    plt.figure()
    plt.title('Train and valid loss')
    plt.plot(data.epoch,loss,label="train_loss")
    plt.plot(data.epoch,val_loss,label="val_loss")
    plt.scatter(data.epoch,data.history['loss'],marker='*')
    plt.scatter(data.epoch,data.history['val_loss'],marker='*')
    plt.legend()
    plt.show()

history = train(X=train_tensors, Y=train_targets,  X_val=valid_tensors, Y_val=valid_targets, model=V3_model)
plot_training(history)
#========================================================
#  模型预测
#========================================================
def img_predict(model, img_path):
    '''判断单张图片'''
    prediction = model.predict(path_to_tensor(img_path))
    index = np.argmax(prediction)
    return index

# 加载最佳的模型参数
V3_model.load_weights(best_weight_url)
img_predict(V3_model, 'D://cats_and_dogs/test/cat/cat.8.jpg')
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341