这篇博客主要参考的是keras官方文档中的“面向小数据集构建图像分类模型”。本文记录了学习这篇文章遇到的问题和自己探索的一些方法。官方文档的大致思路是:首先利用去掉全连接层的VGG-16网络,得到数据集的bottleneck feature,然后自己设计几层全连接层,对其进行训练,最后再微调最后面的几层卷积层。
实现一个模型的流程大致如下:1.数据的读取和预处理部分(包括对训练数据的提升和扩充、生成batch等)2.网络的构建3.对模型的编译(挂载上目标函数、优化函数及相关的一些学习率等超参数)4.模型的训练。下面我逐条说明实现的过程中遇到的一些问题和我自己的浅薄思考:
一、数据读取和处理部分
1.官方文档采用的读取数据的方式是ImageDataGenerator.flow_from_directory():从文件夹中读取数据,并对数据进行预处理和提升,生成batch数据。文件路径下要有cat和dog两个子文件夹,表示两个类别。类别的标签是按照字母顺序生成的,cat是0,dog是1。需要注意的是该路径下必须要有一个文件夹,当我用该函数读取测试集数据时,直接将所有图片放在了路径下,发生了报错。
2.从kaggle上下载的train文件中只有一个文件夹,猫和狗混合在了一块,应该将其分到两个文件夹中。利用os.listdir(‘dirname’)语句,返回指定目录下所有文件的目录名。
import shutil
TRAIN_DIR=''
cat_target_file=''
dog_target_file=''
train_dogs_name=[i for i in os.listdir(TRAIN_DIR)if 'dog' in i]
train_cats_name=[i for i in os.listdir(TRAIN_DIR)if 'dog' in i]
for i in train_dogs_name:
src=TRAIN_DIR+i
dst=dog_target_file+i
shutil.copyfile(src,dst)
for i in train_cats_name:
src=TRAIN_DIR+i
dst=cat_target_file+i
shutil.copyfile(src,dst)
3.验证集问题。什么是验证集,验证集是如何发挥作用的,如何选取验证集,如何利用验证集。
验证集的作用是用于防止过拟合,在keras中的fit函数中有对于validation data的注释:
data on which to evaluate the loss and any model metrics at the end of each epoch. The model will not betrained on this data.
也就是说验证集是在每次epoch后计算的,用于提升模型的loss和accuracy的,模型不会利用验证集来训练(指的是网络的权重不会用验证集的数据训练)。验证集主要是测试当前网络采用的一些超参数是否合理,利用validation-based early stopping(提前停止训练)机制来防止过拟合。要注意的是early stopping需要在模型中设置(下条详细说明),不要认为模型中有了验证集,它自己就会提早停止。
我们可以从训练集中选取验证集,但要注意的是我们是把原训练集分成新的训练集合测试集,新的训练集不能与验证集有重合,验证集选取比例是1/4~1/3.
下面谈下我对验证集的理解:测试集是在全部模型训练好之后拿来测试的,在模型的构建中不能用测试集的任何信息。而为了防止过拟合,我们需要选取一些数据验证下模型的泛化能力,因此验证集不能与训练集有重合。
在keras中设置验证集的方法:可以自己建立验证集,然后在模型训练(fit函数)时,传入validation_data中(文档中的方法)。还有一种更为简便的方法,在fit函数中有个validation_split,可以在原训练集中选取一定比例来作为验证集,但要注意:程序在执行时,先进行的是validation_split,然后再shuffle数据,也就是说验证集可能选取的都是一类的样本(详见官方文档中“keras使用陷阱”)。
4.early stopping机制。在模型训练时(fit函数)可以设置EarlyStopping的回调函数,使得检测值不再发生变化时,训练终止。
例如在本例中的代码如下:
from keras.callbacks import EarlyStopping
early_stopping=EarlyStopping(monitor='val_loss',patience=3)
hist=model2.fit_generator(train_generator,
samples_per_epoch=nb_train_samples,
nb_epoch=nb_epoch,
validation_data=validation_generator,
nb_val_samples=nb_validation_samples,
callbacks=[early_stopping])
EarlyStopping中的monitor是监视的变量,patience是指达到EarlyStopping后再训练patience次。
二、模型的构建
1.本文档运用的网络主要是vgg16。我主要想探讨的是载入权重的问题。
首先看下load_weights的注释:
load_weights(self,filepath, by_name=False):
Loads all layer weights from a HDF5save file.
If `by_name` is False (default) weightsare loaded based on the network's topology,meaning the architecture
should be the same as when the weightswere saved. Note that layers that don't haveweights are not taken into account in the topologicalordering, so adding or removing layers is fine as long as theydon't have weights.
If `by_name` is True, weights areloaded into layers only if they share the same name. Thisis useful for fine-tuning or transfer-learningmodels where some of the layers have changed.
从hdf5的文件中载入所有层的权重,当函数中的参数‘by_name’是False时(这个是默认的),权重是根据网络的拓扑结果载入的,意味着网络的结构应该与保存权重时的网络结构一致。需要注意的是没有权重的图层不会考虑到拓扑结构中。因此我们可以添加或者删除一些没有权重的层。
当'by_name'是true时,载入权重要和名字对应。(也是只考虑有权重的层的名字)
在保存权重后,查看权重文件时,发现没有命名的层后面会有“下划线+数字”出现,例如:x=MaxPooling2D((2, 2), strides=(2, 2))(x),我未给该层命名,保存权重后,会发现该层的名字是“MaxPooling2D_5”而且每次保存后缀的数字都不一样,我觉得这是自动生成用于区分的,对载入权重而言没有什么影响。
而由于我们要输入的图像与ImageNet图像大小不一,需要去掉VGG后面的全连接层,因此我们应只载入前面的卷积层,这里有两个方法,一是直接下载作者提供的no-top版本,二是逐层设置权重:
f =h5py.File(weight_path)
fork in range(f.attrs['nb_layers']):
if k >= len(model.layers):
# we don't look at the last(fully-connected) layers in the savefile
break
g = f['layer_{}'.format(k)]
weights = [g['param_{}'.format(p)] for p inrange(g.attrs['nb_params'])]
model.layers[k].set_weights(weights)
f.close()
Q1.f.attrs['nb_layers']是什么意思?我看h5py中没有'nb_layers'的属性啊?attrs是指向f中的属性,点击右键可以看见这个属性(在HDF5-viewer)
Q2.g= f['layer_{}'.format(k)]的含义,.format的作用
format是格式化的意思,输出g就是format(k)填充到{}上
Q3.weights = [g['param_{}'.format(p)] for p inrange(g.attrs['nb_params'])]的含义
得到的是layer下param_0、param_1等
这里用到的是set_weights(weights),weights设置的大小应与该层网络大小一致,否则会报错。
而我在编写vggnet时,利用了Model型代替了Sequential型,model型开始有个Input占位符,因此整个网络层数较之Sequential相比多一层,只要将上述代码做个小小的修改再载入便没有问题了:
f =h5py.File(weight_path)
fork in range(f.attrs['nb_layers']):
if k >= len(model.layers)-1:
# we don't look at the last(fully-connected) layers in the savefile
break
g = f['layer_{}'.format(k)]
weights = [g['param_{}'.format(p)] for p inrange(g.attrs['nb_params'])]
model.layers[k+1].set_weights(weights)
f.close()
修改了两个地方:
一是判断k的大小,model.layers是包含Input的,有32层,而载入的模型是要前31层的
二是model.layers[k+1],相互对应权重
2.连接两个层的方法
文档中script2利用vgg-16 no top模型得到bottle feature,然后设计几个全连接层,训练。而在script3中需要将这两个模型连接,赋值,然后fineturning,文本中运用的是sequential,model.add(top_model)即可
#buildthe VGG-16 net
model=Sequential()
model.add(...)
...
model.load_weights(vgg16_weights)
#builda classifier model to put on top of the convolutional model
top_model= Sequential()
top_model.add(...)
top_model.load_weights()
model.add(top_model)
3.连接两个Model型。接着上条内容,由于我用到的是Model型,也想按照上述的方法进行分别载入权重,然后连接。首先说明我没有成功,下面只是记录了我的一些思考过程,以便以后查阅:
a.首先将问题明确化:建立了两个模型:model1=Model(x1,x2),model2=Model(x3,x4),(x2,x3维数是一致的)然后对这两个model载入权重,最后这两个模型合并model3=Model(x1,x4)。
b.尝试一:
x=Input(shape=model1.output_shape[1:])
y=Flatten()(x)
y=Dense(256,activation='relu')(y)
y=Dropout(0.5)(y)
y=Dense(1,activation='sigmoid')(y)
model2=Model(x,y)
这个是Model2的详细结构,它可以载入权重,但当model3=Model(input_img,y)时(input_img是model1的输入占位符),报错,因为model2用Input占位符,并没有和前面的model连接,数据断开了。
c.尝试二:
去掉model2的input,但这样model2建立不起来,报错内容如下:
UserWarning:Model inputs must come from a Keras Input layer, they cannot be the output of aprevious non-Input layer. Here, a tensor specified as input to"model_28" was not an Input tensor, it was generated by layermaxpooling2d_60.
Notethat input tensors are instantiated via `tensor = Input(shape)`.
Thetensor that caused the issue was: None
Model的输入层必须是来自于keras的Input layers,它们不能是前面非Input层的输出。这里,张量是由maxpooling2d_60产生的,不是Input tensor。
也就是说Model前面必须有占位符,这个就产生了矛盾。
d.尝试三,逐层赋值,这也是我最后采用的方法
三、模型的编译和训练
1.这里的问题是如何画出loss图,首先在fit中记录下数据,再画图,具体代码如下:
hist=model2.fit_generator(train_generator,
samples_per_epoch=nb_train_samples,
nb_epoch=nb_epoch,
validation_data=validation_generator,
nb_val_samples=nb_validation_samples)
model2.save_weights('FineTurn_model2_weights_path.h5')withopen('FineTurn_loss.txt','w') as f:
f.write(str(hist.history))
上述代码是记录数据,然后根据数据画图:
import numpy as np
import matplotlib.pyplot as plt
history=np.load(npy_dir_path)
history=history.tolist()
acc=history['acc']
loss=history['loss']
val_acc=history['val_acc']
val_loss=history['val_loss']
nb_epoach=np.size(acc)
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('VGG-16 Loss Trend')
plt.plot(loss,'blue',label='Training Loss')
plt.plot(val_loss,'green',label='Validation Loss')
plt.xticks(range(0,nb_epoach))
plt.legend()
plt.show()
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.title('VGG-16 Accuracy Trend')
plt.plot(acc,'blue',label='Training Loss')
plt.plot(val_acc,'green',label='Validation Loss')
plt.xticks(range(0,nb_epoach))
plt.legend()
plt.show()