1. 引言
因項目要求,需要在PocketFlow中添加一套PeleeNet-SSD和COCO的API,具體為在datasets文件夾下添加coco_dataset.py, 在nets下添加peleenet_at_coco.py和peleenet_at_coco_run.py。其中網絡結構和backbone等在師兄把項目交付給我之前已經基本完成,所以我主要的工作就是處理COCO的數據(轉換成tfrecord文件)和簡單更改一些調用的接口。另外PocketFlow中已經包含在VOC上的數據處理,且在檢測任務上,VOC和COCO的數據集標注很接近,唯一的區別就是feature_map:
# VOC
feature_map = {
'image/encoded': tf.FixedLenFeature([], dtype=tf.string, default_value=''),
'image/format': tf.FixedLenFeature([], dtype=tf.string, default_value='jpeg'),
'image/filename': tf.FixedLenFeature((), dtype=tf.string, default_value=''),
'image/height': tf.FixedLenFeature([1], dtype=tf.int64),
'image/width': tf.FixedLenFeature([1], dtype=tf.int64),
'image/channels': tf.FixedLenFeature([1], dtype=tf.int64),
'image/shape': tf.FixedLenFeature([3], dtype=tf.int64),
'image/object/bbox/xmin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/xmax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/label': tf.VarLenFeature(dtype=tf.int64),
'image/object/bbox/difficult': tf.VarLenFeature(dtype=tf.int64),
'image/object/bbox/truncated': tf.VarLenFeature(dtype=tf.int64),
}
# COCO
# change filename(string) to image_id(int64) and remove difficult and truncated
feature_map = {
'image/encoded': tf.FixedLenFeature([], dtype=tf.string, default_value=''),
'image/format': tf.FixedLenFeature([], dtype=tf.string, default_value='jpeg'),
'image/image_id': tf.FixedLenFeature([1], dtype=tf.int64), #
'image/height': tf.FixedLenFeature([1], dtype=tf.int64),
'image/width': tf.FixedLenFeature([1], dtype=tf.int64),
'image/channels': tf.FixedLenFeature([1], dtype=tf.int64),
'image/shape': tf.FixedLenFeature([3], dtype=tf.int64),
'image/object/bbox/xmin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymin': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/xmax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/ymax': tf.VarLenFeature(dtype=tf.float32),
'image/object/bbox/label': tf.VarLenFeature(dtype=tf.int64),
}
所以講道理,即使對TensorFLow並不熟,完成這些API的難度也不大,確實也只用了半天就寫完了,不過之后的調試用了半個月
2. 程序掛起無輸出
最開始是訓練時一開始程序就卡住,和掛起差不多,而且也沒有報錯信息,運行截圖如下:

因為TensorFlow的計算圖機制導致在session.run()
之前,所有的輸出都是node和tensor的信息,所以個人覺得debug比較困難。尤其是你一上來就整個什么都不輸出,我確實有點懵逼,不知道怎么做。上Google搜了搜TensorFlow 卡住 掛起等關鍵詞,基本上有兩種:
- TensorFlow計算和數據讀取是異步的,讀不到數據導致進程掛起,在代碼中加上
tf.train.start_queue_runners(sess=sess)
開啟隊列啥的 - 計算圖中有新增節點的操作,導致每次推導時內存占用越來越多,最后進程掛起
但比較氣人的是,師兄寫的PeleeNet-SSD和VOC的API就沒有這種情況,而且我隨便改batchsize都可以正常跑起來,所以網絡結構應該沒問題。至於隊列,因為用的是dataset那一套的API,兩者也沒什么相關,加上之后還是沒卵用。不過peleenet那塊沒問題,那大概是讀取數據的API有問題。
於是我一行一行的對比(PyCharm Compare With...)后來新增的腳本和師兄之前寫的在VOC上的那套,檢查了3遍,是真的看不出有什么問題...然后又檢查了將COCO數據集轉換為tfrecord的腳本,同上,數據應該沒錯。不過唯一的收獲就是發現bbox的坐標計算有錯,導致gt有問題,隨后改過來了。后來想想,如果gt是錯的話,訓練的模型准確率再高,預測時還是gg,這bug也挺難發現的。
最后求助於師兄,他給我演示了一下怎么用session.run()
調試程序,找出程序卡在哪里。他首先判斷因為讀不到數據,所以排除peleenet_at_coco.py和peleenet_at_coco_run.py的問題(xxx_run.py幾個腳本基本都一樣,也不會出什么錯,另外一個主要定義的網絡結構,是讀取數據之后的操作,所以這兩個都不用怎么檢查)。之后着重調試coco_dataset.py,他找到把數據傳入網絡的最后一步,是一個定義在coco_dataset.py的函數parse_fn
,然后注釋掉中間的代碼,只原樣返回最初的輸入,發現能用session.run()
讀到數據之后再逐步解開剩下代碼的注釋。這樣卡在那一步就能說明哪一塊有問題了,最后發現是卡在了數據預處理preprocess_image()
。
我感覺有點開竅了,但是自己之后又調試了下,發現加上預處理的部分,數據也能讀出來...最后的結論是不加預處理數據可以讀,加上之后有時可以讀,有時會卡住,甚至有點隨機的感覺。把預處理換成了簡單的resize()
統一圖片大小之后,batchsize大點就卡的早點,小的話就晚點。那真的有意思,只能接着看預處理部分了。
3. 報錯:Reduction axis 0 is empty in shape [0, 5028]
預處理主要包括:
- 隨機扭曲顏色
- 隨機裁剪(bbox坐標值相應變化)
- 隨機水平翻轉
- 縮放到固定尺寸
- 減去ImageNet訓練集的RGB均值
PocketFlow項目中的數據預處理(數據增強)來自HiKapok/SSD.TensorFlow, 這哥們代碼寫的很好,還有單元測試,屬實良心。另外知乎上也有兩篇相關的文章:Inside TF-Slim(13) preprocessing(圖像增強相關)和SSD-TensorFlow 源碼解析,以后認真學習一下。
僅保留縮放操作之后,運行COCO訓練的腳本,程序報以下錯誤(VOC仍無問題):


測試時發現,COCO保存若干張圖片后會卡住,而VOC能一直運行。現在終於能確定是數據的問題了,碰巧在COCO保存的圖片中發現有少數圖片沒有bbox框,大概知道問題出在哪里了。看了看之前檢查數據的腳本,媽的居然寫錯了,修改之后,發現了1020張沒有label和bbox標注的圖片,根據image_id在COCO的instances_train2014.json文件的images列表找到了這些圖片,然而在json文件的annotations列表中居然搜不到這些圖片的標注!(如:COCO_train2014_000000542614.jpg)清除tfrecord文件里的這些圖片的相關數據之后,在COCO上也能正常訓練了!
所以調用argmax()
時,這些圖像的bbox為[],就會出現shape的維度為0而報錯的情況。而程序卡住也是因為圖像增強時處理到了這些圖片,batchsize越大,就越容易遇到(1020/118230)。之前因為輸出過自己轉換的tfrecord的文件結構,而且隨機抽取的圖片也能正常顯示,所以就一味的相信數據沒有問題,還是沒有考慮全面,所以浪費了很多時間。
判斷tfrecord文件是否含不帶bbox的圖片的代碼:
import os
import tensorflow as tf
file_list = tf.gfile.Glob(os.path.join('/DATA/coco/tfrecords', 'val-?????-of-?????'))
i = 0
count = 0
for file in file_list:
for string_record in tf.python_io.tf_record_iterator(file):
example = tf.train.Example()
example.ParseFromString(string_record)
i += 1
image_id = example.features.feature['image/image_id'].int64_list.value
xmax = example.features.feature['image/object/bbox/xmax'].float_list.value
# xmin = example.features.feature['image/object/bbox/xmin'].float_list.value
# ymax = example.features.feature['image/object/bbox/ymax'].float_list.value
# ymin = example.features.feature['image/object/bbox/ymin'].float_list.value
if xmax == []:
print(image_id)
count += 1
print(file, 'has been checked.')
print(i, 'images have been checked.')
print(count, 'annotations without bbox.')
4. 總結
- TensorFlow框架雖然有限制,但調試還是可以調試的,首先要熟悉程序的結構,弄清參數傳遞的過程
- 不要糾纏細節,調試的目的是快速且理性的判斷bug的位置
- 寫程序時要寫單元測試,調試時會方便許多
- TensorFLow, to make your life difficult:)
