预训练模型是和迁移学习一起的,也是这几年很热的一个课题。
参考资料
引言
跟传统的监督式机器学习算法相比,深度神经网络目前最大的劣势是什么?
贵。
尤其是当我们在尝试处理现实生活中诸如图像识别、声音辨识等实际问题的时候。一旦你的模型中包含一些隐藏层时,增添多一层隐藏层将会花费巨大的计算资源。
庆幸的是,有一种叫做“迁移学习”的方式,可以使我们在他人训练过的模型基础上进行小改动便可投入使用。在这篇文章中,我将会讲述如何使用预训练模型来加速解决问题的过程。
目录
什么是迁移学习?
什么是预训练模型?
为什么我们使用预训练模型?-结合生活实例
我们可以怎样运用预训练模型?
提取特征(extract features)
优化模型(fine tune the model)
优化模型的方式
在数字识别中使用预训练模型
只针对输出密集层(output dense layer)的重新训练
冻结初始几层网络的权重因子
什么是迁移学习? 为了对迁移学习产生一个直观的认识,不妨拿老师与学生之间的关系做类比。
一位老师通常在ta所教授的领域有着多年丰富的经验,在这些积累的基础上,老师们能够在课堂上教授给学生们该领域最简明扼要的内容。这个过程可以看做是老手与新手之间的“信息转移”。
这个过程在神经网络中也适用。
我们知道,神经网络需要用数据来训练,它从数据中获得信息,进而把它们转换成相应的权重。这些权重能够被提取出来,迁移到其他的神经网络中,我们“迁移”了这些学来的特征,就不需要从零开始训练一个神经网络了 。
通过权重的传递来进行迁移学习和人类在世代交替中通过语言传播知识,是一个道理。
什么是预训练模型? 简单来说,预训练模型(pre-trained model
)是前人为了解决类似问题所创造出来的模型。你在解决问题的时候,不用从零开始训练一个新模型,可以从在类似问题中训练过的模型入手。
比如说,如果你想做一辆自动驾驶汽车,可以花数年时间从零开始构建一个性能优良的图像识别算法,也可以从Google
在ImageNet
数据集上训练得到的inception model
(一个预训练模型)起步,来识别图像。
一个预训练模型可能对于你的应用中并不是100%
的准确对口,但是它可以为你节省大量功夫。
接下来,我会举个例子来说明。
为什么我们要用预训练模型? 上周我一直在尝试解决Crowdanalytix platform
上的一个问题:从手机图片中分辨场景。
这是一个图像分类的问题,训练数据集中有4591
张图片,测试集中有1200
张图片。我们的任务是将图片相应地分到16
个类别中。在对图片进行一些预处理后,我首先采用一个简单的MLP(Multi-later Perceptron)
模型,自定义结构.
在对输入图片(224*224*3)
平整化后,为了简化上述结构,我用了三个各含有500
个神经元的隐藏层。在输出层中,共有16
个神经元对应着十六个类别。
我只能将训练的准确率控制在6.8%
,这是个很不理想的结果。我尝试对隐藏层、隐层中神经元的数量以及drop out
速率进行调整,但准确度都没有太大的提升。而如果增加隐藏层和其中神经元的数量,每个周期的运行时间则会增加20s
以上。(我的开发环境是12GB VRAM
,Titan X GPU
)
除非指数级地增加训练时长,MLP
模型无法提供给我更好的结果。因此,我转而采用CNN
(卷积神经网络)「自定义结构」,看看他们在这个数据集上的表现,以及是否能够提高训练的准确度。
我使用了3
个卷积的模块,每个模块由以下部分组成:
32
个5*5
的filter
线性整流函数(ReLU
)作为激活函数
4*4
的最大值池化层
最后一个卷积模块输出的结果经过平整化后会被传递到一个拥有64
的神经元的隐藏层上,随后通过一个drop out rate = 0.5
处理后传递到输出层。
准确率15.75%
,尽管与MLP模型相比有所提升,但每个周期的运行时间也增加了。
而更重要的是,数据集中最大类别所含图片数量约占总数17.6%
左右。
只要把所有的图片都归到最大的类别,我们就能够得到比MLP
、CNN
训练出来的模型更好的结果。
此外,增加更多的卷积模块也会大大增加训练时长。
于是,我转而去采用预训练模型,这样我不需要重新训练我的整个结构,只需要针对其中的几层进行训练即可。
因此,我采用了在ImageNe
数据集上预先训练好的VGG16
模型,这个模型可以在Keras
库中找到。
在VGG16
结构的基础上,我只将softmax
层的1000
个输出改为16
个,从而适应我们这个问题的情景,随后重新训练了dense layer
。
跟MLP
和CNN
相比,这个结构的准确率能够达到70%
。同时,使用VGG16
最大的好处是大大减少了训练时间,只需要针对dense layer
进行训练,所需时间基本可以忽略。
怎样使用预训练模型? 当在训练经网络的时候我们的目标是什么?我们希望网络能够在多次正向反向迭代的过程中,找到合适的权重。
通过使用之前在大数据集上经过训练的预训练模型,我们可以直接使用相应的结构和权重,将它们应用到我们正在面对的问题上。这被称作是“迁移学习”,即将预训练的模型“迁移”到我们正在应对的特定问题中。
在选择预训练模型的时候你需要非常仔细,如果你的问题与预训练模型训练情景下有很大的出入,那么模型所得到的预测结果将会非常不准确。
举例来说,如果把一个原本用于语音识别的模型用来做用户识别,那结果肯定是不理想的。
幸运的是,Keras
库中有许多这类预训练的结构。
ImageNet
数据集已经被广泛用作训练集,因为它规模足够大(包括120
万张图片),有助于训练普适模型。ImageNet的训练目标,是将所有的图片正确地划分到1000
个分类条目下。这1000
个分类基本上都来源于我们的日常生活,比如说猫猫狗狗的种类,各种家庭用品,日常通勤工具等等。
在迁移学习中,这些预训练的网络对于ImageNet
数据集外的图片也表现出了很好的泛化性能。
既然预训练模型已经训练得很好,我们就不会在短时间内去修改过多的权重,在迁移学习中用到它的时候,往往只是进行微调(fine tune
)。
微调模型的方法
我们可以将预训练模型当做特征提取装置来使用。具体的做法是,将输出层去掉,然后将剩下的整个网络当做一个固定的特征提取机,从而应用到新的数据集中。
我们还可以采用预训练模型的结构,但先将所有的权重随机化,然后依据自己的数据集进行训练。
另一种使用预训练模型的方法是对它进行部分的训练。具体的做法是,将模型起始的一些层的权重保持不变,重新训练后面的层,得到新的权重。在这个过程中,我们可以多次进行尝试,从而能够依据结果找到frozen layers
和retrain layers
之间的最佳搭配。
如何使用与训练模型,是由数据集大小和新旧数据集(预训练的数据集和我们要解决的数据集)之间数据的相似度来决定的。
在修改模型的过程中,我们通过会采用比一般训练模型更低的学习速率。
下图表展示了在各种情况下应该如何使用预训练模型:
场景一:数据集小,数据相似度高(与pre-trained model的训练数据相比而言) 在这种情况下,因为数据与预训练模型的训练数据相似度很高,因此我们不需要重新训练模型。我们只需要将输出层改制成符合问题情境下的结构就好。
我们使用预处理模型作为模式提取器。
比如说我们使用在ImageNet
上训练的模型来辨认一组新照片中的小猫小狗。在这里,需要被辨认的图片与ImageNet
库中的图片类似,但是我们的输出结果中只需要两项——猫或者狗。
在这个例子中,我们需要做的就是把dense layer
和最终softmax layer
的输出从1000
个类别改为2
个类别。
场景二:数据集小,数据相似度不高 在这种情况下,我们可以冻结预训练模型中的前k个层中的权重,然后重新训练后面的n-k
个层,当然最后一层也需要根据相应的输出格式来进行修改。
因为数据的相似度不高,重新训练的过程就变得非常关键。而新数据集大小的不足,则是通过冻结预训练模型的前k层进行弥补。
场景三:数据集大,数据相似度不高 在这种情况下,因为我们有一个很大的数据集,所以神经网络的训练过程将会比较有效率。然而,因为实际数据与预训练模型的训练数据之间存在很大差异,采用预训练模型将不会是一种高效的方式。
因此最好的方法还是将预处理模型中的权重全都初始化后在新数据集的基础上重头开始训练。
场景四:数据集大,数据相似度高 这就是最理想的情况,采用预训练模型会变得非常高效。最好的运用方式是保持模型原有的结构和初始权重不变,随后在新数据集的基础上重新训练。
在手写数字识别中使用预训练模型 现在,让我们尝试来用预训练模型去解决一个简单的问题。
我曾经使用vgg16作为预训练的模型结构,并把它应用到手写数字识别上。
让我们先来看看这个问题对应着之前四种场景中的哪一种。我们的训练集(MNIST
)有大约60,000
张左右的手写数字图片,这样的数据集显然是偏小的。所以这个问题应该属于场景一或场景二。
我们可以尝试把两种对应的方法都用一下,看看最终的效果。
只重新训练输出层 & dense layer
这里我们采用vgg16
作为特征提取器。随后这些特征,会被传递到依据我们数据集训练的dense layer
上。输出层同样由与我们问题相对应的softmax
层函数所取代。
在vgg16
中,输出层是一个拥有1000
个类别的softmax
层。我们把这层去掉,换上一层只有10
个类别的softmax
层。我们只训练这些层,然后就进行数字识别的尝试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 from keras.models import Sequentialfrom scipy.misc import imreadget_ipython().magic('matplotlib inline' ) import matplotlib.pyplot as pltimport numpy as npimport kerasfrom keras.layers import Denseimport pandas as pdfrom keras.applications.vgg16 import VGG16from keras.preprocessing import imagefrom keras.applications.vgg16 import preprocess_inputimport numpy as npfrom keras.applications.vgg16 import decode_predictionstrain=pd.read_csv("R/Data/Train/train.csv" ) test=pd.read_csv("R/Data/test.csv" ) train_path="R/Data/Train/Images/train/" test_path="R/Data/Train/Images/test/" from scipy.misc import imresizetrain_img=[] for i in range(len(train)): temp_img=image.load_img(train_path+train['filename' ][i],target_size=(224 ,224 )) temp_img=image.img_to_array(temp_img) train_img.append(temp_img) train_img=np.array(train_img) train_img=preprocess_input(train_img) test_img=[] for i in range(len(test)): temp_img=image.load_img(test_path+test['filename' ][i],target_size=(224 ,224 )) temp_img=image.img_to_array(temp_img) test_img.append(temp_img) test_img=np.array(test_img) test_img=preprocess_input(test_img) model = VGG16(weights='imagenet' , include_top=False ) features_train=model.predict(train_img) features_test=model.predict(test_img) train_x=features_train.reshape(49000 ,25088 ) train_y=np.asarray(train['label' ]) train_y=pd.get_dummies(train_y) train_y=np.array(train_y) from sklearn.model_selection import train_test_splitX_train, X_valid, Y_train, Y_valid=train_test_split(train_x,train_y,test_size=0.3 , random_state=42 ) from keras.layers import Dense, Activationmodel=Sequential() model.add(Dense(1000 , input_dim=25088 , activation='relu' ,kernel_initializer='uniform' )) keras.layers.core.Dropout(0.3 , noise_shape=None , seed=None ) model.add(Dense(500 ,input_dim=1000 ,activation='sigmoid' )) keras.layers.core.Dropout(0.4 , noise_shape=None , seed=None ) model.add(Dense(150 ,input_dim=500 ,activation='sigmoid' )) keras.layers.core.Dropout(0.2 , noise_shape=None , seed=None ) model.add(Dense(units=10 )) model.add(Activation('softmax' )) model.compile(loss='categorical_crossentropy' , optimizer="adam" , metrics=['accuracy' ]) model.fit(X_train, Y_train, epochs=20 , batch_size=128 ,validation_data=(X_valid,Y_valid))
冻结最初几层网络的权重 这里我们将会把vgg16
网络的前8
层进行冻结,然后对后面的网络重新进行训练。这么做是因为最初的几层网络捕获的是曲线、边缘这种普遍的特征,这跟我们的问题是相关的。我们想要保证这些权重不变,让网络在学习过程中重点关注这个数据集特有的一些特征,从而对后面的网络进行调整。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 from keras.models import Sequentialfrom scipy.misc import imreadget_ipython().magic('matplotlib inline' ) import matplotlib.pyplot as pltimport numpy as npimport kerasfrom keras.layers import Denseimport pandas as pdfrom keras.applications.vgg16 import VGG16from keras.preprocessing import imagefrom keras.applications.vgg16 import preprocess_inputimport numpy as npfrom keras.applications.vgg16 import decode_predictionsfrom keras.utils.np_utils import to_categoricalfrom sklearn.preprocessing import LabelEncoderfrom keras.models import Sequentialfrom keras.optimizers import SGDfrom keras.layers import Input, Dense, Convolution2D, MaxPooling2D, AveragePooling2D, ZeroPadding2D, Dropout, Flatten, merge, Reshape, Activationfrom sklearn.metrics import log_losstrain=pd.read_csv("R/Data/Train/train.csv" ) test=pd.read_csv("R/Data/test.csv" ) train_path="R/Data/Train/Images/train/" test_path="R/Data/Train/Images/test/" from scipy.misc import imresizetrain_img=[] for i in range(len(train)): temp_img=image.load_img(train_path+train['filename' ][i],target_size=(224 ,224 )) temp_img=image.img_to_array(temp_img) train_img.append(temp_img) train_img=np.array(train_img) train_img=preprocess_input(train_img) test_img=[] for i in range(len(test)):temp_img=image.load_img(test_path+test['filename' ][i],target_size=(224 ,224 )) temp_img=image.img_to_array(temp_img) test_img.append(temp_img) test_img=np.array(test_img) test_img=preprocess_input(test_img) from keras.models import Modeldef vgg16_model (img_rows, img_cols, channel=1 , num_classes=None) : model = VGG16(weights='imagenet' , include_top=True ) model.layers.pop() model.outputs = [model.layers[-1 ].output] model.layers[-1 ].outbound_nodes = [] x=Dense(num_classes, activation='softmax' )(model.output) model=Model(model.input,x) for layer in model.layers[:8 ]: layer.trainable = False sgd = SGD(lr=1e-3 , decay=1e-6 , momentum=0.9 , nesterov=True ) model.compile(optimizer=sgd, loss='categorical_crossentropy' , metrics=['accuracy' ]) return model train_y=np.asarray(train['label' ]) le = LabelEncoder() train_y = le.fit_transform(train_y) train_y=to_categorical(train_y) train_y=np.array(train_y) from sklearn.model_selection import train_test_splitX_train, X_valid, Y_train, Y_valid=train_test_split(train_img,train_y,test_size=0.2 , random_state=42 ) img_rows, img_cols = 224 , 224 channel = 3 num_classes = 10 batch_size = 16 nb_epoch = 10 model = vgg16_model(img_rows, img_cols, channel, num_classes) model.summary() model.fit(X_train, Y_train,batch_size=batch_size,epochs=nb_epoch,shuffle=True ,verbose=1 ,validation_data=(X_valid, Y_valid)) predictions_valid = model.predict(X_valid, batch_size=batch_size, verbose=1 ) score = log_loss(Y_valid, predictions_valid)