人工智能競賽-目標識別指導
前言
競賽地址: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對一段視頻中的目標進行預測(難度未知),具體做的是什么,等我下一篇文章出來就知道了。