迁移学习基本概念
迁移学习是这两年比较火的一个话题, 主要原因是在当前的机器学习中, 样本数据的获取是成本最高的一块. 而迁移学习可以有效的把原有的学习经验 (对于模型就是模型本身及其训练好的权重值) 带入到新的领域, 从而不需要过多的样本数据, 也能达到大批量数据所达成的效果, 进一步节省了学习的计算量和时间.
MobileNet V2 是由谷歌在 2018 年初发布的一个视觉模型, 在 keras 中已经内置的并使用 ImageNet 完成了训练, 可以直接拿来就用, 这个我们在本系列第五篇中已经提过了. MobileNet V2 有轻量和高效的特点. 通过 Inverted residual block 结构, 可以用较少的运算量得到较高的精度, 很适合移动端的机器学习需求. 论文在这里 https://128.84.21.199/abs/1801.04381 .
在 ImageNet 数据集上, MobileNet V2 能达到 92.5% 的识别正确率. 本篇中, 我们以此模型为基础, 介绍一个典型的迁移学习实现方法. 并通过调整模型完成优化.
问题描述
MobileNet V2 原本是识别图片中主题的名称. 在 ImageNet 中, 有大量的经过标注的照片, 标注的信息也非常详细. 比如我们熟悉的猫猫, 狗狗, ImageNet 并不简单的标注为 cat 或者 dog, 而是更详细的标注为加菲, 德牧这样精确到具体品种的信息.
我们这里使用调整之后的 MobileNet V2 模型, 用于对图片内容的猫猫和狗狗分类. 不去管原本是哪种具体的品种, 只分成 cat/dog 两个大类.
这个问题的描述实际上隐藏了两个重点:
迁移学习并不是无限制, 随意实现的. 原有学习数据和数据的场景, 同当前的问题, 是有共同点, 可借鉴可迁移的.
把 cat,dog 的具体品种忽略, 简单的分成两类, 并不能认为就是把问题简化了. 要知道人工智能并不是人, 举例来说, 其实机器学习模型自己, 并不知道 "藏獒" 跟 "狗" 之间有什么关系. 在机器学习的模型中, 会认为这是两个不同的分类.
从简单开始, 先展示一下识别
如同本系列第五篇一样, 先使用最简短的代码熟悉一下 MobileNet V2.
我们这个例子所使用的数据, 是使用 tensorflow_datasets 模块来自动下载, 解压, 管理的. 所以请先安装这个扩展包:
$ pip3 install tfds-nightly
程序在第一次运行的时候, 会自动下载微软的实验数据集. 请尽量使用程序自动下载, 因为下载之后会自动解压. 数据集的保存路径为:"~/tensorflow_datasets/", 这个是 tensorflow_datasets 默认的.
数据集中是随机尺寸的图片, 程序第一步会将图片统一到 224x224 的尺寸, 这个是预置的 MobileNet V2 模型所决定的.
我们从样本中取头两个图片显示在屏幕上, 并且使用模型预测图片内容. 请参考源代码中的注释阅读程序, 也可以对照一下第五篇的 vgg-19/ResNet50 模型预测图片内容的程序, 这些模型的使用方法几乎是相同的.
- #!/usr/bin/env python3
- # 引入所使用到的扩展库
- from __future__ import absolute_import, division, print_function
- import os
- import numpy as np
- import matplotlib.pyplot as plt
- import tensorflow as tf
- import tensorflow_datasets as tfds
- keras = tf.keras
- # 载入训练数据, 载入时按照 80%:10%:10% 的比例拆分为训练, 验证, 测试三个数据集
- # 本程序只是演示识别图片, 区分为三类并没有直接意义, 但下面的程序训练模型会使用到
- SPLIT_WEIGHTS = (8, 1, 1)
- splits = tfds.Split.TRAIN.subsplit(weighted=SPLIT_WEIGHTS)
- (raw_train, raw_validation, raw_test), metadata = tfds.load(
- 'cats_vs_dogs', split=list(splits),
- with_info=True, as_supervised=True)
- # 图片标注
- get_label_name = metadata.features['label'].int2str
- # 显示头两幅图片
- i = 1
- plt.figure('Dog & Cat', figsize=(8, 4))
- for image, label in raw_train.take(2):
- plt.subplot(1, 2, i)
- plt.imshow(image)
- plt.title(get_label_name(label))
- i += 1
- plt.show()
- # 所有图片转为 224x224 的尺寸, 适应模型的要求
- IMG_SIZE = 224
- def format_example(image, label):
- image = tf.cast(image, tf.float32)
- image = (image/127.5) - 1
- image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
- return image, label
- train = raw_train.map(format_example)
- # 载入模型, 第一次执行会下载 h5 文件
- # 预测和迁移学习所用的模型并不完全相同, 所以会下载两次
- test_model = tf.keras.applications.MobileNetV2(weights='imagenet')
- # 预测头两张照片并显示结果
- for image, _ in train.take(2):
- img = np.expand_dims(image, axis=0)
- predict_class = test_model.predict(img)
- desc = tf.keras.applications.mobilenet_v2.decode_predictions(predict_class, top=3)
- print(desc[0])
我们执行程序看看预测结果:
- $ ./cats_dogs_1predict.py
- [('n02089078', 'black-and-tan_coonhound', 0.46141574), ('n02105412', 'kelpie', 0.15314996), ('n02106550', 'Rottweiler', 0.092713624)]
- [('n02123045', 'tabby', 0.29928064), ('n02123159', 'tiger_cat', 0.08147916), ('n02096177', 'cairn', 0.047330838)]
程序准确的预测出了结果.
迁移学习改造
我们进行猫, 狗分类训练, 先了解一下样本集. 样本集的图片没有什么区别, 刚才我们见到了. 标注则非常简单, 就是 1(代表 Dog)或者 0(代表 Cat). 上面的程序中, 我们使用 get_label_name(label)将数字转换为可读的字符串. 这个标注比 ImageNet 要简单的多.
MobileNet V2 模型默认是将图片分类到 1000 类, 每一类都有各自的标注. 因为本问题分类只有两类, 所以在代码上, 我们构建模型的时候增加 include_top=False 参数, 表示我们不需要原有模型中最后的神经网络层(分类到 1000 类), 以便我们增加自己的输出层. 当然这样在第一次执行程序的时候, 需要重新下载另外一个不包含 top 层的 h5 模型数据文件.
随后我们在原有模型的后面增加一个池化层, 对数据降维. 最后是一个 1 个节点的输出层, 因为我们需要的结果只有两类.
到了迁移学习的重点了, 我们的基础模型的各项参数变量, 我们并不想改变, 因为这样才能保留原来大规模训练的优势, 从而保留原来的经验. 我们在程序中使用 model.trainable = False, 设置在训练中, 基础模型的各项参数变量不会被新的训练修改数据.
接着我们把数据集分为训练, 验证, 测试三个数据集, 用测试集数据和验证集数据对新的模型进行训练和过程验证. 随后对完成训练的模型, 使用测试集数据进行评估.
请看源代码:
- #!/usr/bin/env python3
- # 引入所使用到的扩展库
- from __future__ import absolute_import, division, print_function
- import os
- import numpy as np
- import matplotlib.pyplot as plt
- import tensorflow as tf
- import tensorflow_datasets as tfds
- keras = tf.keras
- # 载入训练数据, 载入时按照 80%:10%:10% 的比例拆分为训练, 验证, 测试三个数据集
- SPLIT_WEIGHTS = (8, 1, 1)
- splits = tfds.Split.TRAIN.subsplit(weighted=SPLIT_WEIGHTS)
- (raw_train, raw_validation, raw_test), metadata = tfds.load(
- 'cats_vs_dogs', split=list(splits),
- with_info=True, as_supervised=True)
- # cat/dog 两类图片, 复杂度要低于 1000 类的图片, 所以分辨率可以略低, 这样也能节省训练时间
- # 所有图片重新调整为 160x160 点阵
- IMG_SIZE = 160
- def format_example(image, label):
- image = tf.cast(image, tf.float32)
- # 数据规范化为 - 1 到 + 1 的浮点数
- image = (image/127.5) - 1
- image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))
- return image, label
- train = raw_train.map(format_example)
- validation = raw_validation.map(format_example)
- test = raw_test.map(format_example)
- BATCH_SIZE = 32
- SHUFFLE_BUFFER_SIZE = 1000
- train_batches = train.shuffle(SHUFFLE_BUFFER_SIZE).batch(BATCH_SIZE)
- validation_batches = validation.batch(BATCH_SIZE)
- test_batches = test.batch(BATCH_SIZE)
- # 输入形状就是 160x160x3, 最后 3 为 RGB3 字节色
- IMG_SHAPE = (IMG_SIZE, IMG_SIZE, 3)
- base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
- include_top=False, # 使用不包含原有 1000 类输出层的模型
- weights='imagenet')
- # 设置基础模型: MobileNetV2 的各项权重参数不会被训练所更改
- base_model.trainable = False
- # 输出模型汇总信息
- base_model.summary()
- # 增加输出池化层
- global_average_layer = tf.keras.layers.GlobalAveragePooling2D()
- # 输出层
- prediction_layer = keras.layers.Dense(1)
- # 定义最终完整的模型
- model = tf.keras.Sequential([
- base_model,
- global_average_layer,
- prediction_layer
- ])
- # 学习梯度
- base_learning_rate = 0.0001
- # 编译模型
- model.compile(optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate),
- loss='binary_crossentropy',
- metrics=['accuracy'])
- # 各部分数据数量
- num_train, num_val, num_test = (
- metadata.splits['train'].num_examples*weight/10
- for weight in SPLIT_WEIGHTS
- )
- # 迭代次数
- initial_epochs = 10
- steps_per_epoch = round(num_train)//BATCH_SIZE
- # 验证和评估次数
- validation_steps = 20
- # 显示一下未经训练的初始模型评估结果
- loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
- print("initial loss: {:.2f}".format(loss0))
- print("initial accuracy: {:.2f}".format(accuracy0))
- # 训练
- history = model.fit(train_batches.repeat(),
- epochs=initial_epochs,
- steps_per_epoch=steps_per_epoch,
- validation_data=validation_batches.repeat(),
- validation_steps=validation_steps)
- # 评估
- loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
- print("Train1ed loss: {:.2f}".format(loss0))
- print("Train1ed accuracy: {:.2f}".format(accuracy0))
因为数据集比较大, 模型也比较复杂, 所以程序执行起来时间很长. 最终的评估结果类似为:
- Train1ed loss: 0.38
- Train1ed accuracy: 0.95
如果不对比来看, 这个结果还不错啦. 不过准确率比原来对更为复杂的 ImageNet 数据集的评估值还要低很多, 显然还有很大的优化空间.
模型优化
在整个模型中, 我们自己增加的部分很少, 优化的余地并不多. 考虑到原有 ImageNet 图片库的样本, 大多并非猫和狗. 所以完全保留原有的模型参数可能对 MobileNet V2 来讲也是资源上的浪费.
因此我们采取的优化策略就是开放一部分模型的网络层, 允许在进一步的训练中, 调整其权重值, 从而期望更好的结果.
在 MobileNet V2 模型中, 一共有 155 层卷积或者神经网络. 这个值可以使用 len(model.layers)来查看. 我们仍然锁定前面的 100 层, 把后面的网络层打开, 允许训练修改其参数.
在随后新模型的训练中, 也不需要全部重头开始训练, model.fit 方法中, 可以指定 initial_epoch 参数, 接着前面的训练继续进行.
请看源代码:
... 继续前面的代码最后输入...
- # 打开允许修改基础模型的权重参数
- base_model.trainable = True
- # 仍然锁死前 100 层的权重不允许修改
- fine_tune_at = 100
- # 冻结 fine_tune_at 之前的层
- for layer in base_model.layers[:fine_tune_at]:
- layer.trainable = False
- # 重新编译模型
- model.compile(loss='binary_crossentropy',
- optimizer=tf.keras.optimizers.RMSprop(lr=base_learning_rate/10),
- metrics=['accuracy'])
- # 输出模型的概况, 注意观察有些层被锁定, 有些层可以更改
- model.summary()
- # 在前次训练的基础上再训练 10 次
- fine_tune_epochs = 10
- total_epochs = initial_epochs + fine_tune_epochs
- # 训练模型
- history_fine = model.fit(train_batches.repeat(),
- steps_per_epoch=steps_per_epoch,
- epochs=total_epochs,
- initial_epoch=initial_epochs,
- validation_data=validation_batches.repeat(),
- validation_steps=validation_steps)
- # 评估新模型
- loss0, accuracy0 = model.evaluate(test_batches, steps=validation_steps)
- print("final loss: {:.2f}".format(loss0))
- print("final accuracy: {:.2f}".format(accuracy0))
执行优化后的程序, 其中 model.summary()的输出信息:
- Model: "sequential"
- _________________________________________________________________
- Layer (type) Output Shape Param #
- =================================================================
- mobilenetv2_1.00_160 (Model) (None, 5, 5, 1280) 2257984
- _________________________________________________________________
- global_average_pooling2d (Gl (None, 1280) 0
- _________________________________________________________________
- dense (Dense) (None, 1) 1281
- =================================================================
- Total params: 2,259,265
- Trainable params: 1,863,873
- Non-trainable params: 395,392
Non-trainable params 这一部分, 就是指我们锁定不参与新数据训练的参数数量. 如果你注意看前面一部分 base_model.summary()的输出, 所有的参数都被锁定不参与训练.
最后的评估结果为:
- final loss: 0.23
- final accuracy: 0.97
看起来虽然略好, 但似乎优化效果并不明显.
其实这主要是观察数值不够直观造成的, 我们继续为程序增加绘图功能:
... 继续前面的代码最后输入...
- # 优化前训练迭代数据
- acc = history.history['accuracy']
- val_acc = history.history['val_accuracy']
- loss = history.history['loss']
- val_loss = history.history['val_loss']
- # 优化后训练迭代数据
- acc += history_fine.history['accuracy']
- val_acc += history_fine.history['val_accuracy']
- loss += history_fine.history['loss']
- val_loss += history_fine.history['val_loss']
- # 使用训练数据绘图
- plt.figure(figsize=(8, 8))
- plt.subplot(2, 1, 1)
- plt.plot(acc, label='Training Accuracy')
- plt.plot(val_acc, label='Validation Accuracy')
- plt.ylim([0.8, 1])
- plt.plot([initial_epochs-1,initial_epochs-1],
- plt.ylim(), label='Start Fine Tuning')
- plt.legend(loc='lower right')
- plt.title('Training and Validation Accuracy')
- plt.subplot(2, 1, 2)
- plt.plot(loss, label='Training Loss')
- plt.plot(val_loss, label='Validation Loss')
- plt.ylim([0, 1.0])
- plt.plot([initial_epochs-1,initial_epochs-1],
- plt.ylim(), label='Start Fine Tuning')
- plt.legend(loc='upper right')
- plt.title('Training and Validation Loss')
- plt.xlabel('epoch')
- plt.show()
我们为了讲解方便, 把最终的程序分成了几个部分, 实际上为了节省时间, 是可以合并在一起执行的, 这样不需要重复训练很多次.
从绘图结果看, 优化的效果还是很明显的:
两张图, 中间都有一条绿线分隔开优化前和优化后的训练数据. 在前半段, 正确率和损失值的优化过程是明显比较慢的, 而且训练集和验证集两条线的分离也说明有过拟合的现象. 在后半段, 有一个明显的阶梯表现出来模型性能明显改善, 训练集和验证集也更接近. 说明各项指标都有效改善了.
(待续...)
来源: https://www.cnblogs.com/andrewwang/p/10782784.html