人工智能竞赛-目标识别指导
时间:2022-03-21
前言
竞赛地址:https://www.kaggle.com/c/pku-autonomous-driving
这个竞赛是要求训练一个模型,用这个模型对一张二维图片中的汽车进行三维坐标分析,说白了就是通过一张图片判断出汽车的角度、车头的朝向位置之类的信息,这其实是非常困难的,因为用二维图像预测三维坐标需要用的几何的知识。
事实上我在阅读高分作品代码的时候也发现了自己对于几何方面知识的不足,但是本着宁做错也不交白卷的原则,而且这次竞赛给出的图片数据也很多(训练和测试用的图片加起来一共有六千多张),于是我决定趁此机会就学习一下人工智能中的物体检测技术。
前置小知识
在学习物体检测之前需要的前置知识大致有卷积、池化和残差网络,这三种技术都可以提升检测到目标物体的概率,卷积与池化这两个可以放在一起说,这两项技术的作用就是将一张大图片中的重要信息提取出来,大致过程如下图所示:
14x14表示有14x14个像素,3表示RGB三种颜色,从图片的变化来看,每张图片的像素变少了,但是层数增加了,最后从400变成了4是因为最后加了一层全连接层,至于到底为什么这么做能使图像训练效率变高,至今也没有一个准确的定义。
大致的思路就是在图像中的一些大数字代表了某种含义(这里用的池化层是最大池化层),将这些大数字保留下来,过滤掉小数字,就可以过滤掉一些图片中不重要的信息,从而减少训练量(卷积+池化可以有效的减少算力成本)。
至于残差网络,是用来防止梯度爆炸和梯度消失问题的,在训练的时候会因为层数的增加导致在梯度下降的时候产生梯度消失(与x轴水平)或梯度爆炸(与y轴水平)的问题。
为了解决这个问题,何凯明、张翔宇、任少卿和孙剑这四位大佬设计出了残差网络,残差网络说白了就是为激活值提供一条小路,将前面的激活值带到后面层中,这样就解决了梯度爆炸和梯度消失的问题(从训练结果来看也确实是有效的)。
这些前置知识在本例中只需要掌握大概原理就行了,并不需要自己动手从头写一个,卷积、池化、全连接和残差网络这几项技术都是需要许多的数学与算法基础的,并非明白原理就能直接写出
我的代码
yolo介绍
讲完前置知识就要开始讲物体检测技术了,物体检测包含了物体定位与图像分类,图像分类后面再讲,物体定位大致分为三种,第一种是关键点检测法,第二种是滑动窗口检测法,第三种就是YOLO用的我称之为网格检测法。
第一种关键点检测法可以有效检测一个人的面部、身体的姿态,这种检测方法典型的案例就是美颜相机,但也可用于人体位置的检测;第二种滑动窗口检测法,这种检测法即使使用了卷积+池化后所需要的算力依旧很高,并且因为窗口大小的原因,检测准确度不高,所以不推荐;第三种YOLO检测法是将一张完整的图片分割成一个网状图片,并对每一个网格中的图片进行探测,如下图所示:
图中的三个不同的网格尺寸并非是不可更改的,网格的密度越大,所能检测到的图片中细小物体的概率也就越高,而且因为yolo(you only look once)的特点就是只需要将整张图片一次性输入网络中就可以了,不像滑动窗口检测法那样每滑动一次窗口就需要重新输入一次,而且由于滑动窗口的大小的原因,滑动窗口检测法本身的精度也不是很高。
而从上图中可以看到,yolo对于目标物体探测的精度会随着网格密度的增高而增高,也不会存在像滑动窗口那样因为步长过大而滑过了目标物体从而导致探测不到的情况。
而我这次用的是yolov3,yolov3用的是一个全卷积神经网络---- Darknet-53,这个网络和之前说的略有不同,这个网络使用卷积层代替了池化层,这么做的目的是因为池化层(这里指的是最大池化)会将图片中的低层级信息丢弃,这会导致低层级特征的损失,而darknet-53的全卷积设计有效的规避了最大池化的缺点;
在yolo中还有几个问题,那就是在预测中物体有可能被重复探测到,并且如果多个物体出现在同一个位置,那么很有可能会丢弃掉部分物体,如下图所示:
汽车和狗都被重复探测到了,而自行车则完全没有被探测到,H号框则完全探测错误了,为了解决这些问题,yolo引入了非最大值抑制(Non-Maximum Suppression,NMS)和anchor box(其实就是自定义的种类框),简单来说非最大值抑制技术会将两个或多个交并比(ioU)过大的框选择一个概率值最大的将其留下,剩下的则丢弃,并且还会将概率值不足0.5(这个值可以自己设置)的框丢弃,这行就能解决一个物体被重复探测到和像上图中H号框那样完全错误的情况。
狗与自行车重叠的情况则需要用到anchor box,anchor box其实就是在制作数据的阶段定义了很多的不同大小的框,这些框有自己的详细尺寸信息,并且还有类别信息,比如汽车框和狗框,这两个就是完全不一样的,yolo之所以优秀是因为早在yolov2版本时期就已经能探测出九千种物体了(论文中称之为yolo9000)。
在本次使用的yolov3中在最后的图像分类时使用的并不是yolov2的softmax,网上能找到的分析文章有些差异,有的讲是用sigmoid替代了softmax,有的讲是用logistic替代了softmax,这里的sigmoid存疑,就算使用sigmoid也应该换成relu或者leaky relu。
因为relu各方面都优于sigmoid(具体使用的是什么还需要我阅读源码后再确定),不过肯定的是yolov3已经放弃了softmax,因为softmax的分类全都是并列关系,没有包含关系,例如在softmax中“人”“男人”“女人”这三个类只能返回其中一个,而事实上“人”这个类是包含了“男人”“女人”这两个类的,总而言之yolov3已经放弃了softmax;
代码部分
其实并不需要自己实现一个yolo,GitHub上可以直接找到已经写好了的,很多人刚开始学的时候都想全都自己实现一遍,但是站在巨人的肩膀上才能看的更远,所以yolov3我是直接从GitHub上下载了一个保证能完美运行的,并且也已经将模型训练好了,即便如此还是需要自己加上anchor box,然后还要自己实现一个非最大值抑制,代码如下:
import numpy as npimport tensorflow as tfimport cv2from IPython.display import Image,displayfrom tensorflow.keras.models import load_modelfrom yolo_utils import read_classes,read_anchors,yolo_head,preprocess_image,generate_colors,draw_outputs%matplotlib inline ################################################################################################ 过滤概率低的边框# 参数:# box_confidence:装载着每个边框的pc# boxes:装载着每个边框的坐标# box_class_probs:装载着每个边框的80个种类的概率# threshold:阈值,概率低过这个值的边框会被过滤掉## 返回值:# scores:装载保留下的那些边框的概率# boxes:装载保留下的那些边框的坐标# classes:装载保留下的那些边框的种类的索引###############################################################################################def yolo_filter_boxes(box_confidence,boxes,box_class_probs,threshold=.6): # 将pc和c相乘,得到具体某个种类是否存在的概率 box_scores=box_confidence*box_class_probs # 获取概率最大的那个种类的索引 box_classes=tf.argmax(box_scores,axis=-1) # 获取概率最大的那个种类的概率值 box_class_scores=tf.reduce_max(box_scores,axis=-1) # 创建一个过滤器,当某个种类的概率值大于等于阈值时,对应这个种类的filtering_mask中的位置就是true,否则就是false # filtering_mask就是[false,true,...,false,true]这种形式 filtering_mask=tf.greater_equal(box_class_scores,threshold) # 用上面的过滤器过滤掉那些概率小的边框 # 过滤完成后,scores和boxes,classes里面就只装载了概率大的边框的概率值和坐标以及种类索引了 scores=tf.boolean_mask(box_class_scores,filtering_mask) boxes=tf.boolean_mask(boxes,filtering_mask) classes=tf.boolean_mask(box_classes,filtering_mask) return scores,boxes,classes # 模块测试box_confidence=tf.random.normal([13,13,3,1],mean=1,stddev=4,seed=1)boxes=tf.random.normal([13,13,3,4],mean=1,stddev=4,seed=1)box_class_probs=tf.random.normal([13,13,3,80],mean=1,stddev=4,seed=1)scores,boxes,classes=yolo_filter_boxes(box_confidence,boxes,box_class_probs,0.5)print("scores[2]=",scores[2])print("boxes[2]=",boxes[2])print("classes[2]=",classes[2])print("scores.shape=",scores.shape)print("boxes.shape=",boxes.shape)print("classes.shape=",classes.shape)
scores[2]= tf.Tensor(12.552861, shape=(), dtype=float32)
boxes[2]= tf.Tensor([ 3.8289614 0.14167517 -0.03989506 -3.3593693 ], shape=(4,), dtype=float32)
classes[2]= tf.Tensor(46, shape=(), dtype=int64)
scores.shape= (500,)
boxes.shape= (500, 4)
classes.shape= (500,)
################################################################################################ 用非最大值抑制技术过滤掉重叠的边框# 参数:# scores:前面yolo_filter_boxes函数保留下的那些边框的概率值# boxes:前面yolo_filter_boxes函数保留下的那些边框的坐标# classes:前面yolo_filter_boxes函数保留下的那些边框的种类的索引# max_boxes:最多想要保留多少个边框# iou_threshold:交并比阈值,交并比大于这个阈值的边框才会被进行非最大值抑制处理## 返回值:# scores:NMS保留下的那些边框的概率# boxes:NMS保留下的那些边框的坐标# classes:NMS保留下的那些边框的种类的索引###############################################################################################def yolo_non_max_suppression(scores,boxes,classes,max_boxes=20,iou_threshold=0.5): # NMS函数,此函数会返回NMS后保留下来的边框的索引 nms_indices=tf.image.non_max_suppression(boxes,scores,max_boxes,iou_threshold=iou_threshold) # 通过上面的索引来分别获取被保留的边框的相关概率值、坐标以及种类的索引 scores=tf.gather(scores,nms_indices) boxes=tf.gather(boxes,nms_indices) classes=tf.gather(classes,nms_indices) return scores,boxes,classes # 模块测试scores=tf.random.normal([54,],mean=1,stddev=4,seed=1)boxes=tf.random.normal([54,4],mean=1,stddev=4,seed=1)classes=tf.random.normal([54,],mean=1,stddev=4,seed=1)scores,boxes,classes=yolo_non_max_suppression(scores,boxes,classes)print("scores[2]=",scores[2])print("boxes[2]=",boxes[2])print("classes[2]=",classes[2])print("scores.shape=",scores.shape)print("boxes.shape=",boxes.shape)print("classes.shape=",classes.shape)
scores[2]= tf.Tensor(8.208248, shape=(), dtype=float32)
boxes[2]= tf.Tensor([ 5.8494906 -0.32543743 0.8039762 -3.6349177 ], shape=(4,), dtype=float32)
classes[2]= tf.Tensor(-5.3616023, shape=(), dtype=float32)
scores.shape= (20,)
boxes.shape= (20, 4)
classes.shape= (20,)
################################################################################################ 最终的过滤函数# 参数:# yolo_outputs:YOLO模型的输出结果# max_boxes:你希望最多识别出多少个边框# score_threshold:概率值阈值# iou_threshold:交并比阈值## 返回值:# scores:最终保留下的那些边框的概率# boxes:最终保留下的那些边框的坐标# classes:最终保留下的那些边框的种类的索引###############################################################################################def yolo_eval(outputs,max_boxes=20,score_threshold=0.5,iou_threshold=0.5): # 建立3个空list s,b,c=[],[],[] # 后面调用的Yolov3使用了3个规格的网格(13*13,26*26,52*52)进行预测,所以有三组output for output in outputs: # 将YOLO输出结果分成3份,分别表示概率值、坐标、种类索引 box_confidence,boxes,box_class_probs=output # 使用之前实现的yolo_filter_boxes函数过滤掉概率值低于阈值的边框 scores,boxes,classes=yolo_filter_boxes(box_confidence,boxes,box_class_probs,threshold=score_threshold) s.append(scores) b.append(boxes) c.append(classes) # 将3组output的结果整合到一起 scores=tf.concat(s,axis=0) boxes=tf.concat(b,axis=0) classes=tf.concat(c,axis=0) # 使用yolo_non_max_suppression过滤掉重叠的边框 scores,boxes,classes=yolo_non_max_suppression(scores,boxes,classes,max_boxes=max_boxes, iou_threshold=iou_threshold) return scores,boxes,classes yolo_output=(tf.random.normal([13,13,3,1],mean=1,stddev=4,seed=1), tf.random.normal([13,13,3,4],mean=1,stddev=4,seed=1), tf.random.normal([13,13,3,80],mean=1,stddev=4,seed=1))yolo_output1=(tf.random.normal([26,26,3,1],mean=1,stddev=4,seed=2), tf.random.normal([26,26,3,4],mean=1,stddev=4,seed=2), tf.random.normal([26,26,3,80],mean=1,stddev=4,seed=2))yolo_output2=(tf.random.normal([52,52,3,1],mean=1,stddev=4,seed=3), tf.random.normal([52,52,3,4],mean=1,stddev=4,seed=3), tf.random.normal([52,52,3,80],mean=1,stddev=4,seed=3)) # 模块测试yolo_outputs=(yolo_output,yolo_output1,yolo_output2)scores,boxes,classes=yolo_eval(yolo_outputs)print("scores[2]=",scores[2])print("boxes[2]=",boxes[2])print("classes[2]=",classes[2])print("scores.shape=",scores.shape)print("boxes.shape=",boxes.shape)print("classes.shape=",classes.shape)
scores[2]= tf.Tensor(183.36862, shape=(), dtype=float32)
boxes[2]= tf.Tensor([-0.9321569 1.2601769 -0.5666194 -1.3579395], shape=(4,), dtype=float32)
classes[2]= tf.Tensor(23, shape=(), dtype=int64)
scores.shape= (20,)
boxes.shape= (20, 4)
classes.shape= (20,)
# 定义种类已经anchor box和像素class_names=read_classes("model_data/coco_classes.txt")anchors=read_anchors("model_data/yolo_anchors.txt") # 加载已经训练好的YOLO模型yolo_model=load_model("model_data/yolo_model.h5")yolo_model.summary()
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
============================================================
input (InputLayer) [(None, 416, 416, 3) 0
__________________________________________________________________________________________________
yolo_darknet (Functional) [(None, None, None, 40620640 input[0][0]
__________________________________________________________________________________________________
yolo_conv_0 (Functional) (None, 13, 13, 512) 11024384 yolo_darknet[0][2]
__________________________________________________________________________________________________
yolo_conv_1 (Functional) (None, 26, 26, 256) 2957312 yolo_conv_0[0][0]
yolo_darknet[0][1]
__________________________________________________________________________________________________
yolo_conv_2 (Functional) (None, 52, 52, 128) 741376 yolo_conv_1[0][0]
yolo_darknet[0][0]
__________________________________________________________________________________________________
yolo_output_0 (Functional) (None, None, None, 3 4984063 yolo_conv_0[0][0]
__________________________________________________________________________________________________
yolo_output_1 (Functional) (None, None, None, 3 1312511 yolo_conv_1[0][0]
__________________________________________________________________________________________________
yolo_output_2 (Functional) (None, None, None, 3 361471 yolo_conv_2[0][0]
============================================================
Total params: 62,001,757
Trainable params: 61,949,149
Non-trainable params: 52,608
__________________________________________________________________________________________________
# 探测图片img_raw,img=preprocess_image("test.jpg",model_image_size=(416,416))yolo_outputs=yolo_model(img) # 将YOLO模型的输出结果转换成我们需要的格式outputs=yolo_head(yolo_outputs,anchors,len(class_names)) # 过滤边框out_scores,out_boxes,out_classes=yolo_eval(outputs) # 加载图片并进行测试def img_show(image_file,out_scores,out_boxes,out_classes,class_names): img_raw=tf.image.decode_image(open('./images/'+image_file,'rb').read(),channels=3) img=cv2.cvtColor(img_raw.numpy(),cv2.COLOR_RGB2BGR) colors=generate_colors(class_names) # print('在{}图片中找到{}个目标'.format(image_file),len(out_boxes)) print('Found {} boxes for {}'.format(len(out_boxes),image_file)) img=draw_outputs(img,out_scores,out_boxes,out_classes,colors,class_names) display(Image(data=bytes(cv2.imencode('.jpg',img)[1]),width=800)) file_name=[x for x in image_file.split('.')] cv2.imwrite('./res/'+file_name[0]+'_out.'+file_name[1],img) return img # 使用测试训练集进行检测img=img_show('test.jpg',out_scores,out_boxes,out_classes,class_names)
Found 12 boxes for test.jpg
car 1.00 (353, 295) (755, 645)
car 1.00 (761, 286) (923, 404)
car 1.00 (944, 322) (1288, 701)
car 0.99 (925, 294) (1054, 379)
car 0.99 (142, 304) (330, 449)
car 0.91 (709, 281) (778, 347)
car 0.88 (309, 303) (366, 355)
car 0.80 (385, 291) (437, 325)
car 0.75 (673, 279) (720, 321)
traffic light 0.70 (680, 194) (690, 210)
car 0.68 (965, 272) (1025, 290)
truck 0.60 (579, 247) (644, 298)
# 对目标图片进行预测def predict(model,image_file,anchors,class_names): img_raw,img=preprocess_image(image_file,model_image_size=(416,416)) yolo_outputs=yolo_model(img) outputs=yolo_head(yolo_outputs,anchors,len(class_names)) out_scores,out_boxes,out_classes=yolo_eval(outputs) img=img_show(image_file,out_scores,out_boxes,out_classes,class_names) return img img=predict(yolo_model,'ID_6ae2b25af.jpg',anchors,class_names)
Found 15 boxes for ID_6ae2b25af.jpg
car 0.99 (2062, 1859) (2516, 2231)
car 0.98 (1964, 1826) (2218, 2007)
car 0.98 (-11, 1759) (335, 1966)
car 0.94 (1859, 1780) (2074, 1925)
car 0.88 (664, 2372) (3213, 2711)
car 0.78 (1502, 1734) (1576, 1791)
car 0.72 (308, 1704) (428, 1787)
car 0.71 (707, 1712) (814, 1765)
car 0.71 (1758, 1748) (1842, 1823)
car 0.70 (568, 1718) (694, 1769)
car 0.64 (443, 1708) (568, 1770)
car 0.62 (2254, 1769) (2367, 1834)
car 0.60 (1802, 1757) (1895, 1832)
car 0.57 (2177, 1767) (2286, 1831)
car 0.51 (833, 1697) (939, 1758)
import os Epath=os.walk('./images')for path,dir,filelist in Epath: for filename in filelist: img_path = os.path.join(filename) #print(img_path) img=predict(yolo_model,img_path,anchors,class)
这里我就不贴结果了,因为图片有好几千张,论坛肯定是上传不了的。
从预测结果来看,近处的车辆被识别的很好,但是远处的物体识别的就不是很好,而且有的居然还识别出错了,将行人识别成交通信号灯了。
将识别好的图片放大后可以看到远处的信号灯和行人都没有被识别出来,这可能是因为远处的行人在图片上太小了,也有可能是因为数据集中的图片整体偏暗的原因,后续的优化方向可能就是朝着将网格密度加大和将摄像头光圈调大
从测试结果来看,近处的识别没问题,但是远处的识别问题很大,所以这个yolo如果要想用在真实环境下(例如自动驾驶的目标识别)还是需要进一步优化的。
结束语
这个代码我并没有提交,因为我做的和题目要求的完全不一样,属于驴唇不对马嘴了,不过当我重新学习我自己的目标预测代码的时候还是会学到很多之前没有注意到的小细节,比如yolov3并没有池化层、激活函数用的并不是softmax之类的。
关于文章中提到的知识点,比如:卷积、池化、残差网络和激活函数之类的,绝非我一篇文章就能讲明白的,并且因为我自己也不是很聪明,所以理解难免有偏差,如有错误之处,请在评论区指出,多谢!
接下来的目标可能是这个比赛原本要求的结果,也就是通过一张图片判断出汽车的角度、车头的朝向位置之类的信息(这个对我来说比较难),或者是用最新的yolov5来做同样的事情(这个相对简单),又或者是使用任意版本的yolo对一段视频中的目标进行预测(难度未知),具体做的是什么,等我下一篇文章出来就知道了。
上一篇:美国通过“改变游戏规则”的网络事件强制报告立法 下一篇:漏洞挖掘分析技术总结