宠物狗图片分类之迁移学习代码笔记
本文主要是总结之前零零散散抽出时间做的百度西交大狗狗图片分类竞赛题目 竞赛.目前本人已经彻底排到了50名后面,,,也没有想到什么办法去调优,并且平时也忙没时间再继续做.权且记录下来一些过程和心得.
综合说明
代码已经全部上传到github上,地址为:github code.大家下载的时候喜欢的话可以star下哈.
data目录没有上传,这里简单说下目录结构.与src同级的data目录下有train_data2和test1两个目录.分别是训练集和官方测试集.train_data2下是99个目录(原本是100种狗,后来发现有两种其实是一样的只是打标签打的不一样了,官方的回应是就这样了也不修改了,后续我测试发现将这两种合并下效果会略微好一点),每个目录下存放有图片.
训练过程我是将所有训练图片约20000张读入程序,而后分割出5%作为测试集.完成模型训练后去预测test1里图片的标签进而上传到官网评测.官方以错误率为评价指标.
目前本代码测试单模型最好约能到20.09,进行一些模型投票后最好到19.08.然后就再也升不上去了...
不保证运行本代码一定能得到上述我所说的我的最好结果.毕竟模型初始化啊,迭代轮数都会有影响.
主要是将代码撸一遍分析下为自己做笔记.
代码记录分析
main()函数
if __name__=="__main__":
classnum = 99
X,Y = getdata.get('../data/train_data2/')
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, train_size=0.95, random_state=79)
Y_train = makeonehot(Y_train,classnum)
Y_test = makeonehot(Y_test,classnum)
train(X_train,Y_train,X_test,Y_test,depoch=35,ftepoch=50,batch_size=32,classnum=classnum,out='inception.model')
首先是获取数据.使用的是getdata.py里的函数.其主要操作就是读取所有图片并将图片和标签分别存储到X,Y里去.这个没啥好说的,我代码写的比较乱,但是该有的注释还是有的,嗯.
而后使用了sklearn里的train_test_split进行了测试集合和训练集合的分割.这里取了5%作为测试集,其实是比较少的.一般来说我们做实验是至少要取20%作为测试集的,并且一般还要进行交叉验证来进行超参数选择,不过我这里都没有,原因一是我时间不多,二是跑一次程序就得一天多,很多超参数其实是我凭经验设置的,交叉验证实在是跑不起.
而后对标签进行了one_hot处理.这个keras里好像没有像tensorflow里那个sparse_crossentropy_loss的东东啊.有的话告诉我下呗..
然后就去训练了.设置了先depoch个35轮,而后ftepoch个50轮,batch_size为32张,保持的模型前缀叫inception.model等等.
train()函数.
def train(X_train,Y_train,X_test,Y_test,depoch=50,ftepoch=201,batch_size=32,classnum=100,out='inceptionv3-ft.model'):
"""Use transfer learning and fine-tuning to train a network on a new dataset"""
nb_train_samples = len(Y_train)
nb_classes = classnum
nb_val_samples = len(Y_test)
batch_size = batch_size
# data prep
train_datagen = ImageDataGenerator(
preprocessing_function=preprocess_input,
rotation_range=30,#角度
width_shift_range=0.2,#水平偏移
height_shift_range=0.2,#高度偏移
shear_range=0.2,#剪切强度,逆时针方向的剪切变化角度
zoom_range=0.2,#随机缩放的幅度
horizontal_flip=True,#进行随机水平反转
vertical_flip=False#进行竖直反转
)
train_generator = train_datagen.flow(X_train, Y_train, batch_size=batch_size, seed=42)
X_test = preprocess_input(X_test)
# setup model
base_model = InceptionV3(weights='imagenet', include_top=False) #include_top=False excludes final FC layer
model = add_new_last_layer(base_model, nb_classes)
# transfer learning
setup_to_transfer_learn(model, base_model)
for i in range(depoch):
print('Epoch: ',i)
model.fit_generator(train_generator,epochs=1,
steps_per_epoch = int(nb_train_samples/batch_size),
class_weight='auto',workers=30,max_q_size=100)
score1, acc1 = model.evaluate(X_test, Y_test, batch_size=batch_size)
print('epoch: ',i,'eval_acc: ',acc1)
# fine-tuning
setup_to_finetune(model)
for i in range(ftepoch):
print('Epoch: ',i)
model.fit_generator(train_generator,epochs=1,
steps_per_epoch = int(nb_train_samples/batch_size),
class_weight='auto',workers=30,max_q_size=100)
score1, acc1 = model.evaluate(X_test, Y_test, batch_size=batch_size)
print('epoch: ',i,'eval_acc: ',acc1)
if i%10 == 0 and i !=0:
model.save(out+str(i))
score, acc = model.evaluate(X_test, Y_test, batch_size=batch_size)
print('now accu:',acc)
print('ALL DONE')
首先是统计一些参数.
keras的图片数据增强
train_datagen是训练数据生成器.这个属于是keras特有的福利了应该.我们做图像分类的时候一定要进行的一个步骤就是所谓的数据增强,也就是对原图片进行反转翻转切割放缩等变换来扩充训练数据集.而keras里则自带了这样一个增强工具.我们只需要设置好数据增强的各个参数,然后使用flow函数将原数据传入,这个生成器就会源源不断的产生从原数据增强出的数据.这样的话我们训练的时候就可以一直从这里面取出数据来作为训练集.
下面这个小坑是我自己实验总结来的,没有看keras源码,所以可能有错误,烦请大家一定提出来,,,
说到这里有一个小坑有必要提一下,也就是在tarin_generator=train_datagen.flow()时,传入的总数据的个数最好是能够和batch_size能够整除的,不这样做也可以,我先说说这样做的原因然后如果不这样做后续如何做大家自然就知道了.
因为datagen在生成数据时,其实是先将数据shuffle之后,挨个去取batch_size大小,直到取完这一轮或者datagen重置之后才会重新对数据shuffle而后再对数据遍历.比如说数据一共20个,batch_size设置为9,那么开始会取出2个9,在下一轮时因为只剩下2个样本而batch_size为9就会报错.
所以最好是整除关系(可以通过合理设置batch_size或者去除一部分数据使得能够整除这样的方式),那么如果不这么做呢.这里的代码其实就是一个例子.后续在使用train_generator是在这里:
model.fit_generator(train_generator,epochs=1,
steps_per_epoch = int(nb_train_samples/batch_size),
class_weight='auto',workers=30,max_q_size=100)
这里没有报错,我个人感觉一是里面的steps_per_epoch计算的结果的作用是相当于把数据最后不够batch_size的给忽略掉不要了,二是可能fit_generator在每个epoch时会对里面的train_generator做类似重置这样的工作.
迁移学习模型导入及训练
接着说代码.
因为这里使用的迁移学习,也就是使用了预训练模型InceptionV3.InceptionV3是有自己的输入数据预处理方式的,所以这里对x_test也就是测试数据做了下预处理.其实上面的train_datagen里也要有这个预处理过程.并且IncepV3图片输入维度是299X299X3所以传入的X的大小也要匹配.
而后就是导入IncepV3的预训练模型.这里设置weights='imagenet'也就是说导入的模型有训练好的权重,并且这个权重是从imagenet里训练得到的.include_top=False指的是不包含最后的全连接输出层,这个是肯定是因为imagenet是1000分类而我们这里是99分类,我们只使用InceptionV3的前面部分而最后的全连接层我们自己来补充.也就是通过add_new_last_layer()这个函数,如下:
def add_new_last_layer(base_model, nb_classes):
"""
迁移学习
"""
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(FC_SIZE)(x) #new FC layer, random init
x = BatchNormalization()(x)
x = Activation('relu')(x)
x = Dropout(0.3)(x)
predictions = Dense(nb_classes, activation='softmax')(x) #new softmax layer
model = Model(inputs=base_model.input, outputs=predictions)
return model
这里需要注意的是我使用了BN和dropout,并且dropout设置为0.3可以说是比较小,这个属于我自己实验调参感觉这样比较好.
而后就需要先训练我们刚才加的那几层了.这是迁移学习里的一个技巧,也就是我们会先冻结网络一部分去训练另外一部分,这里因为整个网络前面是InceptionV3的预训练权重,我们认为应该不错,而后面是我们自己加的层是随机初始化的,那么需要将训练几轮把新加的这几层权重也训练的差不多才行.这里我们冻结model的前面所有层,如下:
def setup_to_transfer_learn(model, base_model):
for layer in base_model.layers:
layer.trainable = False
model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
然后就是训练过程了.至于训练多少轮这个看实验结果,我这里训练20-40轮都差不多,一般来说对于复杂的问题只训练这最后两层是很难达到高的准确度的,甚至训练集的准确度也提高不了多少,这其实就是欠拟合了,因为模型可调整的参数太少了,无法拟合出这么复杂的问题.不过如果是简单的问题的话有有可能只训练后面加的这几层就行了.
finetune
那么后面就是finetune了,这也是迁移学习里最重要的一步.顾名思义就是一点一点去拟合.一般我们是要看下整个网络结构,而后从后往前开始,先放开一部分层,冻结前面的,使用数据来训练后面的这几层,好了之后再往前放开一部分层,再训练,如此这样直到达到预期的目标.
我这里只进行了一次fintune,因为我发现就这一次就已经很明显的会过拟合了(训练集准确度很高而测试集准确度不升).
def setup_to_finetune(model):
for layer in model.layers[:NB_IV3_LAYERS_TO_FREEZE]:
layer.trainable = False
for layer in model.layers[NB_IV3_LAYERS_TO_FREEZE:]:
layer.trainable = True
model.compile(optimizer=SGD(lr=0.0001, momentum=0.9), loss='categorical_crossentropy', metrics=['accuracy'])
这里从参数看是冻结了前172层而后续的是放开了.至于这个172是如何得知一般我们是要看下Inception的结构,其结构一般是分层次的,每个层次包含多少多少层,通常我们倾向于以它的这个结构上的层次为划分一步一步去解冻去finetune.
然后就还是训练过程了.这里每10轮保存下模型.主要是为了后续的投票机制.
结尾
到这里基本就完了.想要提高模型效果的话就是不断的调参和上trick去测试了.
有一个技巧很多提到就是使用centerloss,这个原理的话可以参考我一篇博客centerloss.不过目前我只在tensorflow上写这个centerloss,keras好像封装的很深我目前没有太深入了解keras还真实现不出来...那篇博客可以是一个原理性的解释和说明这样的.
另外,本文代码主要参考keras文档里的例子.