基于模型融合的Distracted Driver Detection

项目概述

实验室课题一部分——疲劳驾驶检测系统,巧合发现kaggle上的竞赛:State Farm公司举办的一个竞赛项目,其目的是得到一个可检测司机驾驶行为的模型。
State Farm在很多汽车仪表盘上安装摄像头,截取司机驾驶过程中的2D图片,希望Kaggle参赛者能基于这些驾驶图片,训练一个可检测驾驶行为的模型。
这里,基于该项目,使用近几年在LSVRC大赛上取得突破性进展的深度卷积神经网络模型,在这些预训练模型的基础上,利用迁移学习对本项目的数据集进行训练,最终获得不错效果。
具体代码见 https://github.com/wisdom-bob/distracted_driver_detection
在这里插入图片描述

问题阐述

该项目本质上是一个监督分类学习的问题。训练数据集已经做好了分类标注。模型的目标是学习不同驾驶行为的特征,以致能正确识别一个未见过驾驶图片。

本项目所用数据集来自State Farm收集的驾驶图片数据库。可从Kaggle的Districted_Driver_detection项目中下载数据集,已做好初步分类,得到按c0-c9分类好的train set(c0~c9分别对应不同的驾驶行为,即作为同文件夹的图片标签)以及未分类的test set

数据预处理思路:为了减少过拟合的可能性,首先按驾驶员ID对训练集按照一定比例分割为训练集和验证集,这里是22:4,近似5:1是一个合适的比例,根据标签分类,使用keras的生成器数据增强的同时,shuffle序列重排,到此完成数据准备,当实际训练时还需要数据增强,图片resize。另外需要一提的是:如果数据不够干净的话,需要进行异常数据剔除,数据集我测试了一下,基本干净(无明显标签错误图片,无图片质量太差导致特征不可见,不相干图片等),也就没有去做剔除,但是不要忽视,数据处理还是非常重要的,数据清理这里有一个样例,可见1

迁移学习(微调):我的思路主要建立在迁移学习上,基于keras架构测试过许多模型,最后选择了Xception,InceptionV3,ResnetInceptionV3。数据准备好之后,修改网络模型,设置lambda层,对输出数据进行批归一化处理(正则化),设置初始权重为“imagenet”,设置GlobalAveragePool、Dropout和softmax激活层替换原模型输出层,基于双优化器调参,利用早停获取结果最好的模型权值.h5文件;此时,就可以本地载入h5文件完成预测,平均三模型结果得到最终结果;把结果保存到.csv文件,每一行记录一个被测图片的10种行为分类的概率。最后将该csv文件上传到Kaggle,根据该项目的判断指标来对预测结果进行评分。

数据分析

在这里插入图片描述
数据集包括训练集和测试集,所有数据集均是从车载摄像头录像中截取下来的静态图片。其中训练集已经做了基于label-class的分类,分类为[c0,…,c9],其含义依次是:-c0:安全驾驶;-c1:右手打字;-c2:右手打电话;-c3:左手打字;-c4:左手打电话;-c5:调收音机;-c6:喝饮料;-c7:拿后面的东西;-c8:整理头发和化妆;-c9:和其他乘客说话。
训练集中相同分类的图片在同一个文件夹中,各个分类的图片数量分布见上图。测试集没有做分类,一共包含79721张2D图片。数据集的图片尺寸均是640x480。除了数据集,该项目还提供了司机id列表,该列表包含了数据集中图片对应的司机id及对应的分类,从上图可以看出不同司机的图片分类的分布。如下所示为随机提取的图片样例。在这里插入图片描述
可以很明显发现项目难点所在:图片都是从同一个角度拍摄,图片环境相似度极高,有一定可能性导致过拟合;不同司机图片数量不同,驾驶员特征各异,数量多的特征很容易成为模型学习的主要特征,这是我们所不希望看到的;图片明暗程度较差,可能导致部分特征缺失;此外个别图片还存在干扰的人物因素。
解决办法:从数据集的分布我们可知每项分类的学习数据基本充足均等,我们希望模型学到的是检测图片当中的司机行为,而非衣着或汽车内饰。已知训练集共26位司机,为了降低司机或汽车的属性影响到训练效果,我们要排除模型对相同司机的依赖,即模型训练和验证使用完全不同的司机/汽车id图片
*具体做法;;根据司机id分类训练图片,对司机id index进行K_Fold处理,得到trainset和validationset分别是不同的司机id对应的图片,如trainset的司机id有[p015,p022,p051],validationset的司机id有[p002,p081]。

# 创建文件夹
def rmrf_mkdir(dirname):
    if os.path.exists(dirname):
        shutil.rmtree(dirname)
    os.mkdir(dirname)
# 传输图片到目标文件夹
def transfer_image(driver,targetpath):
    path = "./imgs/train"
    images = datacsv.loc[(datacsv['subject']==driver),['img']]
    classnames = datacsv.loc[(datacsv['subject']==driver),['classname']] 

    for i in range(len(images)):
        resource = "".join(os.path.join(path,classnames.values[i][0],images.values[i][0]))
        target = "".join(os.path.join(targetpath,classnames.values[i][0],images.values[i][0]))
        shutil.copy(resource,target)
    return len(images)

# 根据驾驶员分割训练集和验证集
x_drivers = []
y_drivers = []
datacsv = pd.read_csv('driver_imgs_list.csv')
x_drivers = datacsv['subject'].unique()[:22]
y_drivers = datacsv['subject'].unique()[22:]

# 基于x_drivers&y_drivers重新构建文件树,并导入对应图片到对应标签文件夹
# 重新建立目标文件树
rmrf_mkdir("./data")
print('/data/ recreate') 
os.makedirs("./data/test/val")
for c in ["c0","c1","c2","c3","c4","c5","c6","c7","c8","c9"]: 
    os.makedirs("./data/train/{0}".format(c))
    os.makedirs("./data/validation/{0}".format(c))
print('folders created !') 

# 复制文件到目标文件夹下
tra_nb = 0
val_nb = 0
for driver in x_drivers:
    tra_nb += transfer_image(driver, targetpath="./data/train")
print ("train_data transfer is finished")
for driver in y_drivers:
    val_nb += transfer_image(driver,targetpath="./data/validation/")
print ("validation_data transfer is finished")
for file in os.listdir("./imgs/test/"):
    resource = "".join(os.path.join("./imgs/test/",file))
    target = "".join(os.path.join("./data/test/val",file))
    shutil.copy(resource,target)
tex_nb = len(os.listdir("./data/test/val/"))
print ("test_data transfer is finished")
print (tra_nb, val_nb, tex_nb)

评价标准

既然是神经网络的项目,那么评估标准就至关重要,本项目评价标准为:预测时长和准确率。
Log Loss:Kaggle对预测结果评分使用multi-class logarithmic loss,也称cross-entropy,计算公式为:
在这里插入图片描述
N为测试条目数;M为标签数;yij为某条目的某标签编号;pij某条目是某标签的概率。
Time:一个预测算法能应用到实际产品中,训练和预测时长也很关键。走神检测对实时性要求高,否则没有意义,目前计划准确率在90%以上,预测时间越短更好。

具体方法

数据增强

前面我们对数据进行了预处理,但还不足以直接喂给模型,必须数据增强,详见2,3
运用ImageDatagenerator作为数据提取器,提取图片的同时也提取了该图片对应的分类,保证图片和分类总是一一对应。

# 数据增强,扩大数据集同时,转换数据标签为二进制,划分batch,resize图片,乱序处理
gen = ImageDataGenerator()
train_gen = ImageDataGenerator(rotation_range=10.,width_shift_range=0.05,                            height_shift_range=0.05,shear_range=0.1,zoom_range=0.1)
train_generator = train_gen.flow_from_directory("./data/train",  (299, 299), shuffle=True, batch_size=32, class_mode="categorical")
valid_generator = gen.flow_from_directory("./data/validation",  (299, 299), shuffle=True, batch_size=32, class_mode="categorical")

图片还需要进行Resize,为减少数据量,现将图片进行缩放,为使用预训练模型,我们将图片尺寸缩放成预训练模型能接受的尺寸。这需要根据所选择的预训练模型进行辨别,将图片缩放为299x299,并转化为Numpy array。
将图片的分类进行数据转换,由于我们训练使用的指标是cross entropy,所以我们将图片分类的数值转换成二进制的分类矩阵。最后,我们需要将提取出的数据进行乱序排列,以免受到原始数据顺序的干扰。
当然,归一化和标准化也是必不可少的,对于不同的模型,其批处理也不相同,如VGG、Resnet需要标准化;InceptionV3、InceptionResnetV2、Xception需要归一化,这里直接通过模型修改时lambda层即可实现。

模型修改

这里主要是对迁移学习的一种运用,只改变了输出层,而保持其他层基本不变,详见4`5。见代码吧,改动并不大。

def Model_fit(size,MODEL,preprocess_fun,optimizer,History,tune=0,Epochs=10,drop=0.3):
	# set the input size
    width = size[0]
    height = size[1]
    input_tensor = Input((height, width, 3))
    # set the batch preprocess
    x = Lambda(preprocess_fun)(input_tensor)
    
    # instead of the output layers, set the globalAveragePooling,Dropout and softmax layers
    base_model = MODEL(input_tensor=x, weights='imagenet', include_top=False)
    new_output = GlobalAveragePooling2D()(base_model.output)
    new_output = Dropout(drop)(new_output)
    new_output = Dense(10, activation="softmax")(new_output)
    new_model = Model(base_model.input, new_output)
    if tune:
    # whether to download the trained weights
        new_model.load_weights("./a_%s.hdf5"%MODEL.__name__)
    
    # set the optimizer and loss methods
    new_model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])
#     new_model.summary()
	
    history = LossHistory()
    earlystopping = EarlyStopping(monitor="val_loss", min_delta=0.0003, patience=5, verbose=1, mode="min")
    if tune:
        checkpoint = ModelCheckpoint("./%s.hdf5"%MODEL.__name__,monitor="val_loss", mode="min", save_best_only=True, period=1)
    else:
        checkpoint = ModelCheckpoint("./a_%s.hdf5"%MODEL.__name__,monitor="val_loss", mode="min", save_best_only=True, period=1)

    model = new_model.fit_generator(train_generator, train_generator.samples//32+1, Epochs, verbose=1,
                                    validation_data=valid_generator, callbacks=[history,earlystopping,checkpoint])
    
    History["%s"%MODEL.__name__] = model.history
    print("Finish!")
    return new_model

调试细节

该项目的实现尝试多种方法,包括自己构建简单卷积模型,InceptionV3、InceptionResnetV2、Xception预训练模型,其他预训练模型也尝试过,最终选择这三个表现最好的模型,作为项目使用模型。在训练过程中最关键的就是调参,确保自己的模型能够取到最好结果,主要通过试错法,选取合适的参数范围,多次试验,观察其验证集训练效果,确认最合适的dropout、learning rate。当然,其中调参遵循的准则如下:
1.如果 val_loss 还在下降,那么应该多训练几代,或者增加模型复杂度;
2.如果 val_loss 开始上升,则可能是过拟合问题,需要正则化(dropout等);
3.如果 val_loss 振荡,那就是学习率太大,需要减小学习率;
4.如果 val_loss 已经完全稳定,那么应该减小训练代数。
其主要试验思路,基于Imagenet权重为基础,放开权重限制,基于adam优化器训练三个模型,并提出最佳模型权重hdf5文件,再利用sgd优化器进行精调,从而得到单模型的test结果。
为了防止过拟合,采取以下方法:
Adam粗调,sgd精调
为了加快模型权重的训练,在编译阶段,所有模型首先使用adam进行优化,选择出最优权重,并用sgd进一步微调,使模型收敛效果更好。
Early Stopping
在使用深度CNN时,往往需要很多epoch模型才能收敛。但需要多少epoch模型才收敛,这是一个经验问题,如果epoch过少则训练不完备,如果epoch过多则训练时间过长,并且可能会导致overfitting。
使用Earlystopping的方法来解决这个经验问题。具体而言,我们使用两种earlystopping方法:设定‘val_loss=1e-5’,如果验证集的损失小于这个值时则训练停止;监控‘val_loss’,如果验证集损失有5次迭代都没有提高,则停止训练。
可视化辅助调试
使用CAM技术可视化训练好的CNNs模型,CNNs的高层卷积层往往「记忆」了目标分类的位置信息,通过将对目标分类敏感的filters按权重累加,即可得到目标分类的映射mapping,也就可以直到模型是否正确检测到目标。
CAM具体实现大致是,将深层的某卷积层的输出,与最后某目标分类的权重做数量积,再映射回image space,这样就得到目标分类的可视化图像。该图像颜色深的部位就代表该目标分类的定位。在这里插入图片描述
在这里插入图片描述
heatmap我们可以明显看到,InceptionV3并没有正确学习到特征,或者说对于特征的识别出现了错误。这原因可能是因为图中人物的肤色对图像边缘等形状做了遮挡,导致特征错误识别;手机的位置在方向盘车标处附近,导致手机特征被忽略;InceptionResnetV2和Xception正确根据手机的识别,确认分类为texting_right。那么把三个模型的预测结果进行均值处理,就可以消除InceptionV3的错误结果,实现正确分类。
从上面几组图片可以充分证明前述所说,三种模型的预测结果可能存在差异,甚至截然不同的项目,那么通过综合多种模型的预测结果可以提高分类准确率。

最终调试结果参数表如下:
parameter for training InceptionV3
在编译阶段,优化器使用Adam,loss_function使用categorical_cross_entropy,metrics使用accuracy,设定lr=0.0003,dropout=0.3,迭代次数15,batchsize:32。
微调阶段,optimizer为sgd,lr=0.0001,decay=1e-6,momentum=0.9,nesterov=True,dropout=0.4。
parameter for training InceptionResNetV2
在编译阶段,优化器使用Adam,loss_function使用categorical_cross_entropy,metrics使用accuracy,设定lr=0.0001,dropout=0.4,迭代次数15,batchsize:32。
微调阶段,optimizer为sgd,lr=0.0001,decay=1e-6,momentum=0.9,nesterov=True,dropout=0.5。
parameter for training Xception
在编译阶段,优化器使用Adam,loss_function使用categorical_cross_entropy,metrics使用accuracy,设定lr=0.0001,dropout=0.5,迭代次数15,batchsize:32。
微调阶段,optimizer为sgd,lr=0.00005,decay=1e-6,momentum=0.9,nesterov=True,dropout=0.5。

结果

在这里插入图片描述
从上表,我们可以直观发现,Adam单步的得分普遍要略差于Adam&SGD分步优化,特别是val_loss,当低于0.35时,一点点的提升都是至关重要的,而单Adam优化很难做到。
当然,选择分步优化的好处,类似于锁层分步优化,保证部分效果较优的基础上进行进一步优化,但其缺点就在于时间必然会延长。(锁层分步优化:锁住模型部分层的权重,重点优化某些层,保证从表层特征识别到抽象特征的学习等各层效果最好。)
使用Pretrained InceptionV3
Fine-tune InceptionV3最后得到的测试结果,private-score为0.25093,kaggle排名138/1440。
在这里插入图片描述
使用Pretrained InceptionResNetV2
Fine-tune InceptionResNetV2得到的测试结果,private-score为0.28072,排名 173/1440。
在这里插入图片描述
使用Pretrained Xception
Fine-tune Xception得到的测试结果,private-score为0.29234,排名 185/1440。
在这里插入图片描述
比较以上结果,可以看出,预训练模型,这些深度增大的模型学习效果优于我们构建的简单卷积模型,也说明预训练模型用于迁移学习非常有效。
比较三种模型,从训练数据(见导出.html文件)可见,对比val_acc,InceptionResnetV2远高于其他模型,但与Xception相同存在抖动、容易过拟合的现象,不易得到最优解,从稳定性、准确率等综合考虑,InceptionV3表现最好。
Merge Xception, InceptionV3 and InceptionResNetV2
我们综合了Xception、InceptionV3和InceptionResNetV2的预测结果,得到private-score为0.21150,排名81/1440。从得分上来看,综合多个模型的表现最好,得分已经达到了Leaderboard的Top-6%,达到了本项目的预期目标。

结论

了解过简单CNN模型无法胜任这类较大数据库的多分类问题,本项目直接使用InceptionV3,InceptionResnetV2,Xception等预训练模型进行训练和数据测试,并确定优化方案,再对三种预训练模型结果进行比较和合并结果,选出最优解决方案。
本项目最后取得Kaggle Leaderboard Top6%的结果,主要得益于两点原因,1.迁移训练的使用,利用各种预训练模型让我们在进行数据训练的时候事半功倍;2.多模型的数据融合,极大的提高了模型训练效果;3.模型微调对于最终提高模型效果有非常直观的作用;4.在数据集的处理中根据司机id分训练集和测试集,减少了模型学习到不期望的特征。

有事私信,侵权必删,感谢~


  1. https://github.com/kylechenoO/Dogs_vs_Cats ↩︎

  2. https://blog.csdn.net/zhelong3205/article/details/81810743 ↩︎

  3. https://blog.csdn.net/u010801994/article/details/81914716 ↩︎

  4. https://blog.csdn.net/linolzhang/article/details/73358219 ↩︎

  5. https://blog.csdn.net/u010159842/article/details/79202107 ↩︎

  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值