迁移学习是这两年比较火的一个话题,主要缘由是在当前的机器学习中,样本数据的获取是成本最高的一块。而迁移学习能够有效的把原有的学习经验(对于模型就是模型自己及其训练好的权重值)带入到新的领域,从而不须要过多的样本数据,也能达到大批量数据所达成的效果,进一步节省了学习的计算量和时间。python
MobileNet V2是由谷歌在2018年初发布的一个视觉模型,在Keras中已经内置的并使用ImageNet完成了训练,能够直接拿来就用,这个咱们在本系列第五篇中已经提过了。MobileNet V2有轻量和高效的特色。经过Inverted residual block结构,能够用较少的运算量获得较高的精度,很适合移动端的机器学习需求。论文在这里。
在ImageNet数据集上,MobileNet V2能达到92.5%的识别正确率。本篇中,咱们以此模型为基础,介绍一个典型的迁移学习实现方法。并经过调整模型完成优化。bash
MobileNet V2本来是识别图片中主题的名称。在ImageNet中,有大量的通过标注的照片,标注的信息也很是详细。好比咱们熟悉的猫猫、狗狗,ImageNet并不简单的标注为cat或者dog,而是更详细的标注为加菲、德牧这样精确到具体品种的信息。
咱们这里使用调整以后的MobileNet V2模型,用于对图片内容的猫猫和狗狗分类。不去管本来是哪一种具体的品种,只分红cat/dog两个大类。
这个问题的描述实际上隐藏了两个重点:网络
如同本系列第五篇同样,先使用最简短的代码熟悉一下MobileNet V2。
咱们这个例子所使用的数据,是使用tensorflow_datasets模块来自动下载、解压、管理的。因此请先安装这个扩展包:app
$ 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()
咱们为了讲解方便,把最终的程序分红了几个部分,实际上为了节省时间,是能够合并在一块儿执行的,这样不须要重复训练不少次。
从绘图结果看,优化的效果仍是很明显的:
两张图,中间都有一条绿线分隔开优化前和优化后的训练数据。在前半段,正确率和损失值的优化过程是明显比较慢的,并且训练集和验证集两条线的分离也说明有过拟合的现象。在后半段,有一个明显的阶梯表现出来模型性能明显改善,训练集和验证集也更接近。说明各项指标都有效改善了。 (待续...)