keras封装好的model怎么修改全连接_Keras做图片分类(四):迁移学习--猫狗大战实战

c99ce4c4e4a578022d968d30a77e7f3b.png

本项目数据集来自kaggle竞赛,地址:

https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/data

数据的训练集放在train文件夹下,测试集放在test文件夹下,其中train文件夹下的图片命名方式为cat.0.jpg,一直到cat.12499.jpg,然后是dog.0.jpg直到dog.12499.jpg,共25000张图片。测试集图片命名格式为1.jpg~12500.jpg共12500张图片。我们需要用训练集对模型进行训练,然后在测试集上“考试”,提交kaggle查看考试结果。

图片载入

这里介绍两种图片的载入方式。

第一种方法将所有图片加载成ndarray格式,在本系列第一讲就介绍过了,这里再讲一种简单些的处理方法。

import cv2
import numpy as np
from tqdm import tqdm

def load_train(n, img_size):
    X = np.zeros((n, img_size, img_size, 3), dtype=np.uint8)
    y = np.zeros((n, 2), dtype=np.uint8)

    for i in tqdm(range(n//2)): #tqdm给载入过程增加了进度条
        X[i] = cv2.resize(cv2.cvtColor(cv2.imread('train/cat.%d.jpg' % i), cv2.COLOR_BGR2RGB), (img_size, img_size)) #读入图片 + 转成RGB + resize
        X[i+n//2] = cv2.resize(cv2.cvtColor(cv2.imread('train/dog.%d.jpg' % i), cv2.COLOR_BGR2RGB), (img_size, img_size))

    y[:n//2, 0] = 1 #one-hot编码0为(1,0),1为(0,1)
    y[n//2:, 1] = 1
    return X,y

def load_test(n,img_size):
    X = np.zeros((n, img_size, img_size, 3), dtype=np.uint8)

    for i in tqdm(range(n)):
        #test图片从1.jpg开始
        X[i] = cv2.resize(cv2.cvtColor(cv2.imread('test/%d.jpg' % (i+1)), cv2.COLOR_BGR2RGB), (img_size, img_size)) 
    return X

第二种载入方式比较简单,借用Keras的ImageDataGeneratorflow_from_directory函数,直接从目录生成数据生成器,很方便。在使用之前需要按照要求布置图片集,将不同类别的图片放到不同的文件夹,具体来讲,train/cat下放所有猫的图片,train/dog下放所有狗的图片。

from keras.preprocessing.image import ImageDataGenerator

batch_size = 16
gen = ImageDataGenerator() #实例化
train_generator = gen.flow_from_directory("train", image_size, shuffle=False, 
                                              batch_size=batch_size)
test_generator = gen.flow_from_directory("test", image_size, shuffle=False, 
                                             batch_size=batch_size, class_mode=None)

这里要注意,测试集由于没有label,生成test_generator的函数需加参数class_mode=None

迁移学习模型

上篇文章讨论过图片归一化预处理时,ndarray的dtype会变成float,内存占用是uint8格式的4倍,如果直接全部载入可能会造成内存错误OOM。所以我们选择在模型中进行预处理。

首先第一步,选择我们要使用的预训练模型,这里以ResNet50为例,看keras是如何进行迁移学习的。

from keras.applications import *

base_model = ResNet50(input_tensor=inputs, weights='imagenet', include_top=False)

这里解释一下,keras将一些表现比较好的预训练模型做进了库里,我们可以直接用函数调用。其中input_tensor需传入一个tensor,weights可以选择None也就是只加载整个模型不加载权重,一般在训练集图片基本与'imagenet'中的class无关时我们选择从头训练模型,这里我们要借用其权重所以weights='imagenet'include_top表示是否去掉最后的全连接层,由于原模型有1000个类别而我们只有2个类别,所以需要去掉然后自己搭建最终的全连接层。

这里我们用预训练模型提取训练集的特征向量来进行预测,具体做法就是使用模型和权重让训练集正向传播,在最后一层后面(ResNet50去掉全连接最后一层为AveragePooling )进行全局平均池化(gap),得到特征向量。

这次需要搭建一个到gap的模型,并把预处理函数放入模型内。

from keras.models import *
from keras.layers import *

input_tensor = Input((224, 224, 3))
inputs = input_tensor
x = Lambda(resnet50.preprocess_input)(inputs) #preprocess_input函数因预训练模型而异
base_model = ResNet50(input_tensor=x, weights='imagenet', include_top=False)
x = base_model(x)
outputs = GlobalAveragePooling2D()(x)
model = Model(inputs, outputs)

看下此时的模型结构

8cf74da16deeefc40dcadae98d05519f.png

得到了模型,接下来正向传播得到GAP层的特征向量。

对于第一种图片载入方法,有以下两种方法提取特征向量

先得到训练数据和测试数据

X_train, y_train = load_train(25000,224)
X_test = load_test(12500,224)

第一种方法,直接predict。

train_features = model.predict(X_train)
test_features = model.predict(X_test)

第二种采用ImageDataGenerator中的flow函数,用生成器的方式,此种方式可以进行数据增强。

datagen = ImageDataGenerator() 
train_generator = datagen.flow(X_train,batch_size=16, shuffle=False)
test_generator = datagen.flow(X_test,batch_size=16, shuffle=False)

train_features = model.predict_generator(train_generator,  verbose=1) 
test_features = model.predict_generator(test_generator,  verbose=1)

采用第二种图片载入方法,即使用flow_from_directory函数得到数据生成器,提取特征向量:

train_features = model.predict_generator(train_generator,  verbose=1) 
test_features = model.predict_generator(test_generator,  verbose=1)

至此,我们提取出了本数据集的特征向量(bottleneck features),训练集特征向量的shape为(25000,2048),测试集的为(12500,2048)。

有一点需要注意,提取特征向量进行预测的做法只有在数据集与'imagenet'高度类似的情况下才可以进行,即只对特征向量到分类层的全连接进行训练,前面模型的层全部冻结。当然,为了进一步提高精度,可以用训练集在预训练模型imagenet权重的基础上继续训练,得到更好的特征向量来预测,这是提高方向。

以此时的特征向量作为训练集来进行预测

X_train = train_features
X_test = test_features

搭建最后的全连接层并进行训练

from keras.callbacks import ModelCheckpoint

inputs = Input((X_train.shape[1:]))
x = Dropout(0.4)(inputs)
x = Dense(2, activation='softmax')(x)
model = Model(inputs, x)

from sklearn.model_selection import train_test_split
X_train,X_val,y_train,y_val = train_test_split(X_train,y_train,test_size=0.2,random_state=0)

checkpointer = ModelCheckpoint(filepath='weights.best.hdf5', 
                               verbose=1, save_best_only=True) #保存最好模型权重
model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

epochs = 10

history = model.fit(X_train, y_train,
          validation_data=(X_val, y_val),
          epochs=epochs,callbacks=[checkpointer],verbose=1)

多模型融合

既然我们提取特征向量只训练最后的全连接层,那么是不是可以将多个模型的特征向量相串接进行融合呢?深度学习从不嫌特征多,只怕特征差。Garbage in,garbage out。

考虑到不同的预训练模型需要不同的输入图片尺寸和预处理函数,这里使用函数统一处理:

import h5py
from keras.applications import *
from keras.models import *
from keras.layers import *
from keras.preprocessing.image import ImageDataGenerator

def FeatureeExtract(MODEL,img_size,func=None)    

    inputs = Input((img_size,img_size,3)) #实例化一个tensor
    x = inputs
    x = Lambda(func)(x) #增加预处理函数层

    base_model = MODEL(input_tensor=x, weights='imagenet', include_top=False) 
    model = Model(base_model.input, GlobalAveragePooling2D()(base_model.output))

    datagen = ImageDataGenerator() #后续可考虑数据增强
    train_generator = datagen.flow(X_train,batch_size=16, shuffle=False)
    test_generator = datagen.flow(X_test,batch_size=16, shuffle=False)

    train_features = model.predict_generator(train_generator,  verbose=1) 
    test_features = model.predict_generator(test_generator,  verbose=1)

    # 保存bottleneck特征
    with h5py.File('%s_data.h5'%MODEL.__name__) as h:
        h.create_dataset("train",data = train_features)
        h.create_dataset("test",data = test_features)    
        h.create_dataset('label',data = y_train)

在keras文档中的预处理函数,根据在imagenet数据集上的预测准确率,排行前三的是InceptionResNetV2、Xception、InceptionV3,考虑用这三个模型进行融合。

FeatureExtract(InceptionResNetV2, 299, inception_resnet_v2.preprocess_input)
FeatureExtract(Xception, 299, xception.preprocess_input)
FeatureExtract(InceptionV3, 299, inception_v3.preprocess_input)

将提取出的特征向量分别保存为h5文件储存,以便我们复现现在的结果,现在提取出来并串接在一起。

X_train = []
X_test = []
for filename in ['InceptionResNetV2_data.h5','InceptionV3_data.h5','Xception_data.h5']:
    with h5py.File(filename,'r') as h:
        X_train.append(np.array(h['train']))
        X_test.append(np.array(h['test']))
        y_train = np.array(h['label'])
X_train = np.concatenate(X_train,axis=1)#将三个模型得到的X_train拼接
X_test = np.concatenate(X_test,axis=1)

from sklearn.utils import shuffle 
np.random.seed(10)  #可以设置随机种子,以后每次打乱都是一样的。
X_train,y_train = shuffle(X_train,y_train)

然后跟上面单模型一样进行训练即可。

8e25ce3a806fe15d0c54a5ba7040864e.png

dfc89ea826ba2e74e66a8ea5cda2a863.png

10个epoch内的训练曲线,效果很好,验证准确率达到了99.82%,可以对测试集进行预测并提交kaggle进行测试查看成绩了。

model.load_weights('weights.best.hdf5')

y_pred = model.predict(X_test)
y_pred = y_pred.ravel() #y_pred变为一维
y_pred = y_pred.clip(min=0.005,max=0.995) #把预测结果clip到[0.005,0.995]
Num = []
imgs = os.listdir("test/")
for i in range(len(imgs)):
    Num.append(int(imgs[i].split('.')[0]))

import pandas as pd
df = pd.DataFrame({'label':y_pred},index=Num)
df.sort_index(inplace=True) #对DataFrame以index排序
df2 = pd.DataFrame({'id':np.arange(1,nb_test+1),'label':df['label']})
df2.to_csv('submit.csv',index=None)

这里有个小trick,由于训练集与验证集同分布,而与测试集分布略有差异,得到的预测结果需要做一个clip,将[0,1]clip到[0.005,0.995]。原因是最终loss的计算方式特性:

8d1254c5595243b98beb2213ce37676d.png

由上式logloss计算方法可以看出,在某个样本结果预测正确的时候logloss为0,这当然很好,但如果样本预测错误,实际为0预测为1(或者实际为1预测为0),其logloss为+∞,这当然是无法接受的。由此将预测结果限制在[0.005,0.995],就算极端的预测错误其单个logloss也只有2,对整个大局的影响不大。

最终提交kaggle的得分达到了0.03798,在public leaderboard上排名第8。

后续工作

  • 实际上在做本项目的时候我进行异常值处理,剔除了一些异常的图片。这些图片里有不是猫狗标成猫狗的,也有是猫狗但是太小无法识别的,也有一些其他情况。由于篇幅原因不在这里展开,只说下做法。利用预训练模型的imagenet权重对所有图片运行预测,然后对照imagenet中猫和狗的类别index,如果预测的top30里都没有猫和狗,把它判定为异常图片。可以用多个预训练模型得到的异常图片进行并集,最终剔除了200多张。
  • 考虑到25000张训练集图片已经足够,并且训练过程中内存占用不小,再加上现阶段已经可以达成基准模型的要求,尚未考虑数据增强。实际上可使用ImageDataGenerator对图片进行数据增强,即对图片采用旋转角度、上下左右平移等操作生成新的图片以扩充训练集。
  • 提取特征向量之前先用训练集在模型'imagenet'权重的基础上再训练,找到正确率最高的特征向量,然后再进行多模型融合。
  • 本项目融合了三个预训练模型,还可考虑更多更好模型的融合。

相关阅读:

stawary:Keras做图片分类(一):图片的导入与处理​zhuanlan.zhihu.com
2790d083e99d4e67afe6e8e7e61d0b89.png
stawary:Keras做图片分类(二):图片的分批读取和数据增强​zhuanlan.zhihu.com
a92d7c9c0502d7381ab4480710d41a53.png
stawary:Keras做图片分类(三):Keras CNN模型cifar-10实战​zhuanlan.zhihu.com
034abca77605fcd818ce91549725cb48.png

——————————————————————————————————————

喜欢我的文章,或者希望了解更多机器学习、人工智能等相关知识、动态的小伙伴可以关注公众号,并进交流群。这里有一群志同道合的小伙伴可以一起交流学习、转行、打比赛等诸多经验。

6de47932eed3667adf627c610c997fb6.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值