什么是迁移学习(Transfer Learning)?简单的理解就是使用一些已经训练好的模型迁移到类似的新的问题进行使用,而不必对新问题重新建模,从头训练和优化参数。这些训练好的模型同时包含了优化好的参数,在使用的时候只需要做一些简单的调整就可以应用到新问题中了。这里是把之前的知识一个实践应用,废话不多说开始。。。
目录
3.4、基于tf.keras、eager、Estimators的ResNet50模型训练
一、概念
传统的机器学习/数据挖掘只有在训练集数据和测试集数据都来自同一个feature space(特征空间)和统一分布的时候才运行的比较好,这意味着每一次换了数据都要重新训练模型,以至于过于麻烦。对于绝大多数开发者来说,在实践中很少有人从头开始训练整个卷积网络(使用随机初始化),因为拥有足够大小的数据集相对较少。即便是不同的事物,feature space都有一定的共通性,这使得在训练过的成熟网络同样通过微调、部分修改后适用于相近的一系列问题。故迁移学习是十分高效的一种学习方式。迁移学习是把一个领域(即源领域)的知识,迁移到另外一个领域(即目标领域),使得目标领域能够取得更好的学习效果。通常,源领域数据量充足,而目标领域数据量较小,迁移学习需要将在数据量充足的情况下学习到的知识,迁移到数据量小的新环境中。
为什么需要迁移学习:
-
数据的标签很难获取,当有些任务的数据标签很难获取时,就可以通过其他容易获取标签且和该任务相似的任务来迁移学习;
-
从头建立模型是复杂和耗时的,也即是需要通过迁移学习来加快学习效率。
迁移学习(打造自己的图像识别模型)其实就是利用已有的深度神经网络(VGG16,AlexNet , GoogLeNet等)进行简单的微调。一般有如下几种方式:
- 只训练全连接层。
- 全部网络重新训练(使用已有参数/从头开始)
- 只训练部分网络。
在实践中,由于数据集不够大,很少有人从头开始训练网络。常见的做法是使用预训练的网络来重新fine-tune,或者当做特征提取器。常见的两类迁移学习场景:
- Finetuning the convnet:我们使用预训练网络初始化网络,而不是随机初始化,就像在imagenet 1000数据集上训练的网络一样。其余训练看起来像往常一样。
- ConvNet as fixed feature extractor: 在这里,我们将冻结除最终完全连接层之外的所有网络的权重。最后一个全连接层被替换为具有随机权重的新层,并且仅训练该层。
例如卷积网络当做特征提取器。使用在ImageNet上预训练的网络,去掉最后的全连接层,剩余部分当做特征提取器(例如AlexNet在最后分类器前,是4096维的特征向量)。得到这样的特征后,可以使用线性分类器(Liner SVM、Softmax等)来分类图像。 Fine-tuning卷积网络。替换掉网络的输入层(数据),使用新的数据继续训练。Fine-tune时可以选择fine-tune的全部层或部分层。通常,前面的层提取的是图像的通用特征(generic features)(如边缘检测,色彩检测),这些特征对许多任务都有用。后面的层提取的是与特定类别有关的特征,故fine-tune时常常只需要采用较小的学习率来Fine-tuning后面的层。
在使用迁移学习的过程中不同场景的一些准则:
- 待训练的数据集较小,已训练的模型和当前任务相似。只重新训练已有模型的靠近输出的几层,例如将ImageNet中输出层原来可以判别一万种输出的网络改为只判别猫的品种,从而利用已有网络来做低层次的特征提取。
- 待训练的数据集较小,已训练的模型和当前任务场景差距较大。例如你有的已训练网络能识别出白天高速路上的违章车辆,你需要训练一个能识别出夜间违章车辆的模型,由于不管白天夜晚,交通规则是没有变化的。故只需将网络靠近输入的那几层重新训练,等到新的网络能够提取出夜间车辆的基本信息后,就可用预训练模型,而不是从头开始。
- 待训练的数据集较大,已有模型的训练数据和现有的训练数据类似。使用原网络的结构,并保留每一层的节点权重,再逐层微调。
- 待训练的数据集较大,已有的模型和新模型的数据差异度很高。从头开始,重新训练。
二、工程实践中的建议
在实际使用迁移学习中,建议最好是目标任务与迁移学习的任务的数据分布相同,这样可以得到更高的准确率。在迁移学习中选择预训练模型也很重要,应该根据不同的使用场景选择合适的框架,主要的选取原则如下:
- 单独使用预训练模型:如果样本充足,则可以首先精度最高的模型;如果样本不足,则可以选择Resnet模型;
- 嵌入到模型中的预训练模型:根据实际功能来选择;
- 如果模型的输入尺寸固定,优先考虑Resnet模型;
- 如果输入尺寸不确定,可以考虑VGG这类模型。
- 在嵌入式上运行的模型:优先选择裁剪后的模型。
另外,通常迁移学习常用于样本不足的情况,此时使用迁移学习的结果可能低于原有的模型。但对于样本充足的情况,可以从头开始训练,而不需要迁移学习。
三、基于Tensorflow二分类的迁移学习实践
3.1、数据集说明
下载链接:https://download.pytorch.org/tutorial/hymenoptera_data.zip。这是取自于imageNet的非常小的子集。其训练集和验证集的数目见下表:
类别 | 训练集 | 验证集 |
---|---|---|
蜜蜂 | 121 | 83 |
蚂蚁 | 124 | 7 |
数据集的结构如下:
3.2、数据加载
这里我们使用tf.data.Dataset接口来加载数据,具体实现如下:
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
import os
import numpy as np
from sklearn.utils import shuffle
def load_sample(sample_dir,shuffleflag = True):
'''递归读取文件。只支持一级。返回文件名、数值标签、数值对应的标签名'''
print ('loading sample dataset..')
lfilenames = []
labelsnames = []
for (dirpath, dirnames, filenames) in os.walk(sample_dir):#递归遍历文件夹
for filename in filenames: #遍历所有文件名
#print(dirnames)
filename_path = os.sep.join([dirpath, filename])
lfilenames.append(filename_path) #添加文件名
labelsnames.append( dirpath.split('\\')[-1] )#添加文件名对应的标签
lab= list(sorted(set(labelsnames))) #生成标签名称列表
labdict=dict( zip( lab ,list(range(len(lab))) )) #生成字典
labels = [labdict[i] for i in labelsnames]
if shuffleflag == True:
return shuffle(np.asarray( lfilenames),np.asarray( labels)),np.asarray(lab)
else:
return (np.asarray( lfilenames),np.asarray( labels)),np.asarray(lab)
def _distorted_image(image,size,ch=1,shuffleflag = False,cropflag = False,
brightnessflag=False,contrastflag=False): #定义函数,实现变化图片
distorted_image =tf.image.random_flip_left_right(image)
if cropflag == True: #随机裁剪
s = tf.random_uniform((1,2),int(size[0]*0.8),size[0],tf.int32)
distorted_image = tf.random_crop(distorted_image, [s[0][0],s[0][0],ch])
distorted_image = tf.image.random_flip_up_down(distorted_image)#上下随机翻转
if brightnessflag == True:#随机变化亮度
distorted_image = tf.image.random_brightness(distorted_image,max_delta=10)
if contrastflag == True: #随机变化对比度
distorted_image = tf.image.random_contrast(distorted_image,lower=0.2, upper=1.8)
if shuffleflag==True:
distorted_image = tf.random_shuffle(distorted_image)#沿着第0维乱序
return distorted_image
from skimage import transform
def _random_rotated30(image, label): #定义函数实现图片随机旋转操作
def _rotated(image): #封装好的skimage模块,来进行图片旋转30度
shift_y, shift_x = np.array(image.shape[:2],np.float32) / 2.
tf_rotate = transform.SimilarityTransform(rotation=np.deg2rad(30))
tf_shift = transform.SimilarityTransform(translation=[-shift_x, -shift_y])
tf_shift_inv = transform.SimilarityTransform(translation=[shift_x, shift_y])
image_rotated = transform.warp(image, (tf_shift + (tf_rotate + tf_shift_inv)).inverse)
return image_rotated
def _rotatedwrap():
image_rotated = tf.py_func( _rotated,[image],[tf.float64]) #调用第三方函数
return tf.cast(image_rotated,tf.float32)[0]
a = tf.random_uniform([1],0,2,tf.int32)#实现随机功能
image_decoded = tf.cond(tf.equal(tf.constant(0),a[0]),lambda: image,_rotatedwrap)
return image_decoded, label
def _norm_image(image,size,ch=1,flattenflag = False): #定义函数,实现归一化,并且拍平
image_decoded = image/127.5-1#image/255.0
if flattenflag==True:
image_decoded = tf.reshape(image_decoded, [size[0]*size[1]*ch])
return image_decoded
def dataset(directory,size,batchsize,random_rotated=False,shuffleflag = True):#定义函数,创建数据集
""" parse dataset."""
(filenames,labels),_ =load_sample(directory,shuffleflag=False) #载入文件名称与标签
#print(filenames,labels)
def _parseone(filename, label): #解析一个图片文件
""" Reading and handle image"""
image_string = tf.read_file(filename) #读取整个文件
image_decoded = tf.image.decode_image(image_string)
image_decoded.set_shape([None, None, None]) # 必须有这句,不然下面会转化失败
image_decoded = _distorted_image(image_decoded,size)#对图片做扭曲变化
image_decoded = tf.image.resize_images(image_decoded, size) #变化尺寸
image_decoded = _norm_image(image_decoded,size)#归一化
image_decoded = tf.to_float(image_decoded)
label = tf.cast( tf.reshape(label, [1,]) ,tf.int32 )#将label 转为张量
return image_decoded, label
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))#生成Dataset对象
if shuffleflag == True:#乱序
dataset = dataset.shuffle(10000)
dataset = dataset.map(_parseone) #有图片内容的数据集
if random_rotated == True:#旋转
dataset = dataset.map(_random_rotated30)
dataset = dataset.batch(batchsize) #批次划分数据集
dataset = dataset.prefetch(1)
return dataset
3.3、tf.keras搭建ResNet50模型
这里搭建ResNet模型采用tf.keras框架来进行搭建。我们使用ResNet50作为该分类模型的基础网络,并下载其预训练模型进行迁移学习,具体实现步骤如下:
- 预训练模型下载
预训练模型的下载地址:https://github.com/fchollet/deep-learning-models/releases。每一种模型都对应两个文件:一个是正常的模型文件,该文件能够直接预测得到输出结果;另一个对应以no-top结尾的文件,该文件是提取特征的模型,用于Finetuning或嵌入模型的场景。同时,如果是Keras运行在Theano框架上,则文件中的‘tf’可以替换为‘th’。在Theano上运行的模型与在Tensorflow上运行的模型的最大区别:图片的维度默认顺序不同,在Theano中通道维度在前,如:(3,224,224),而Tensorflow中刚好相反。这里我们下载的模型为:resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5。具体下载位置如下图:
- ResNet50模型搭建
这里我们使用tf.keras接口搭建ResNet模型并将其作为一个网络层,在ResNet50之后,我们还需要添加两个全连接层并使用sigmoid激活函数对最后一层进行处理,这里我们还需要对分类个数进行修改,得出我们最终的分类:ants和bees。具体代码如下:
from tensorflow.keras import layers,models
from tensorflow.keras.applications.resnet import ResNet50
def resnet50(num_classes=10000,
weights="resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5",
image_size=(224,224,3)):
inputs=layers.Input(shape=image_size) #设置输入数据的形状
base_model=ResNet50(weights=weights,
input_tensor=inputs,input_shape=image_size,include_top=False) #创建Reset50模型
#搭建整个模型
model=models.Sequential()
model.add(base_model)
model.add(layers.Flatten())
#添加全链接层
model.add(layers.Dense(256,activation='relu'))
model.add(layers.Dense(num_classes,activation='sigmoid'))
#冻结ResNet50模型的参数
base_model.trainable=False
return model
#定义模型
num_classes=2
model= resnet50(num_classes,weights=None)
model.summary()
网络结构如下:
3.4、基于tf.keras、eager、Estimators的ResNet50模型训练
- 基于tf.keras的ResNet50模型训练
具体代码实现如下:
import tensorflow as tf
from tensorflow.keras import optimizers
from load_data import dataset
from model import resnet50
size = [224, 224]
batchsize = 32
sample_dir = r"./hymenoptera_data/train"
testsample_dir = r"./hymenoptera_data/val"
#数据加载
traindataset = dataset(sample_dir, size, batchsize) # 训练集
testdataset = dataset(testsample_dir, size, batchsize) # 测试集
#定义模型
model= resnet50(num_classes=2)
model.summary()
learning_rate=0.001# 学习率
model.compile(loss="binary_crossentropy",
optimizer=optimizers.Adam(lr=learning_rate),
metrics=['acc'])
model.fit(traindataset,epochs=20, validation_data=testdataset,shuffle = True)
出现错误:invalid argument: input must be 4-dimensional[1,1,300,300,3]
解决方法:主要是数据集中出现异常数据,输入数据要求“RGB”三通道,而数据集中出现异常数据。检查数据集去除“gif”格式数据集。
训练结果:
发现虽然训练集准确率很高,但是测试集的准确率不变,发现需要在迁移学习时更新BN层,否则它会直接使用之前的BN层数据。这里需要更改模型,具体代码如下:
backend.set_learning_phase(0)#开启训练模式
base_model=ResNet50(weights=weights,input_tensor=inputs,input_shape = image_size,include_top=False)
backend.set_learning_phase(1)#关闭训练模式
参考连接:Keras的BN你真的冻结对了吗
最终的训练结果如下:
参考链接:
Pytorch学习记录-使用Pytorch进行深度学习,迁移学习Transfer Learning
深度学习计算机视觉实战(二) :pytorch上实现对小数据集的迁移学习Transfer Learning(附源码及详细注释)