参考文章:SSD关键源码解析-知乎 目标检测|SSD原理与实现
参考代码:balancap/SSD-Tensorflow
真是佩服那些能从头到尾把算法实现好的人。。该有多难啊。
现阶段成果:成功运行单种类目标检测
1.遇到最多的问题就是 list, array, tensor互相转换了。大部分时间消耗在这个上面。
解决:tensor --> array:
a: tensor; b: array
with tf.Session() as sess:
b = sess.run(a)
list --> array:
a: list; b: array
b = np.array(a, dtype = 'float32')
2.实际VGG输出layer的shape与论文里的不相同。
论文里的layer的shape为(38, 38),(19, 19),(10, 10),(5, 5),(3, 3),(1, 1)。但是按照VGG论文的VGG-16来,输入300*300*3的时候,输出的shape有时候与要求的不一样。后来看了别人的文章,发现是padding的问题。
3.建立框框的scale。
参考的balancap的代码,里面的scale设置的与原文不一样,不知道他这样设置有没有经过试验。暂时按照原文来设置。
4.读/写tfrecord。
一开始打算将图片文件、标注信息做成tfrecord,以方便后续的读取。遇到的困难有:进制变换。将cv2读到的图片变成byte流存到tfrecord里,然后读出来的时候要进一步解码。遇到了各种编解码问题,就很烦。
读出来的文件是tensor的,喂给placeholder的时候还需要把它变成array,也就是说,图片-->array,变成tensor存好,再读出来变成array,怎么想想都是多此一举。
4.1.tensorflow的placeholder不接受feed为tensor的情况!百思不得其解。。为什么不接受tensor。。
然后,一次读完所有的tfrecord文件的话,必然就OOM了。
4.2.OOM报错。
分步读取的话,就老是报下列的错:
Coordinator stopped with threads still running
没想明白是为什么,最后就直接读图片了,反而省事,也很快。
5.图片滑窗/resize。
为了检验功能是否正常,使用了简单的resize,而不是滑窗。不管图片的比例,尺寸,通通resize到300*300。性能肯定是有下降的。如果有机会的话,用一下滑窗试试,顺便可以当成是Data Augmentation。
6.还是feed的问题。参考着balancap的代码,写着写着发现,place holder的输入,变成a list of tensor了(关联错误4)。尝试将a list of tensor变为一个tensor,但是并没有成功,如果需要这样的话,就要在bounding gt和先验框的算法里,将其重新变为array,或list of tensor。想想就,太麻烦了。最后解决的方法为,每次在train的循环里先算出一部分,然后将算出来的单一tensor,feed到placeholder里面去,就解决这个问题了。
7.anchor的尺寸。
一开始的时候没有发现,针对不同的layer,anchor的尺寸是不一样的。layer尺寸从大到小,每个点的anchor数目分别为4, 6, 6, 6, 4, 4。6比4多了1*1和sqrt(Sk * Sk+1)。然后一开始设计的时候,没考虑sqrt(Sk * Sk+1),反正就是说S要多算一个。
另外第一层,也就是conv4_1的话,计算方法是不一样的,独立的。
8.各种tensor乘法!加法!减法!根本无从判断算法写的是不是对的。。
9.在loss里面,有一个weighted_loss,要求loss tensor和weight tensor相乘。傻傻地弄错了weight tensor的大小,导致tensor过大,直接OOM了。(我还在想1080ti 11G怎么连batch_size是4都不够。。)
10.报错
Attempting to use uninitialized value
说是没初始化。。但是我ess.run(tf.global_variables_initializer())了的。。
不知道该怎么办。。训练是能训练了。收敛也收敛了。保存模型一直不行。。
解决,参考我的另一篇文章。。
11.越训练越慢。
知乎讨论 发现是在循环中增加了新的op。使用tf.graph.finalize()使计算图变为只读。也就是说,如果有新的op被创建,就会报错了。
暂时发现这个行的op来自降维,其实是删除长度为1的维度,也就是tf.squeeze()。在程序中,为了将输入的list of array 变为tensor以参与后续的运算,用了tf.squeeze(),然后它每次都会创建新的op,然后内存就越来越少了,越来越卡。
解决方案:重构了绑定ground_truth和anchor的模块。将原有的基于tensor的程序改成了基于numpy的逻辑。(顺便还精简了代码逻辑。。)
12.报错
unhashable type: 'numpy.ndarray' error
stackoverflow上有人说是因为feed_dict中使用的numpy变量类型与placeholder中的不一样。比如tf.float32和np.float64。但是我的不是这个问题。我的代码问题出在feed_dict中的值与placeholder重名了。(很愚蠢的问题)
13.绑定ground_truth和anchor的原理。直接把我代码里的注释复制过来了。
#------------------------------------------------------------------------------------
#This loop does have some thing special. Take feature_labels as example.
#At the begining, feature_scores == 0. After loop, feature_scores is
#almost impossible to be zero. The jaccard score continue updates. Using mask,
#the value of feature_labels --> the label(class) with higher jaccard score.
#It will not change if new jaccard score is less than existing feature_score.
#As the loop goes on. The feature_labels --> label(class) with highest jaccard score.
#Same as feature_labels, other 4 'array' sotre the highest jaccard score
#label(class)'s localization.
#------------------------------------------------------------------------------------
14.ground_truth和anchor绑定时的encode问题。。
15.上面那个问题也在内。训练不收敛。分类是正确的,但是,localizations,也就是box一直不收敛。
解决:
一:首先看了Smooth L1 loss,是正确的,没写错。虽然balancap的代码里用的是简化版的Smooth L1,但我用完全版肯定没问题。
二:看了训练的网络参数,是正常的。我想训练的一直在训练,不想训练的一直没在训练。
三:只用一种正样本来调试,发现问题了。绑定先验框和ground truth的时候出了问题。发现正样本的个数非常之多,并在正样本密集的图片内,几乎有一大半是正样本先验框。这跟常识和论文是不符的。
然后发现balancap的代码里有一行这个:
mask = tf.greater(jaccard, feat_scores)
mask = tf.logical_and(mask, feat_scores > -0.5)
mask = tf.logical_and(mask, label < num_classes)
问题就出在第二行。如果看过他的代码的话,就会发现feat_scores是永远大于0的。所以第二行是无效的行。在我的代码里把它优化掉了。
而SSD论文里要求的是,jaccard > 0.5, 也就是ground truth框和先验框IoU大于0.5的时候,才把这个先验框作为正样本。
这里是balancap的代码出了错误了。看他的issues里也有人反映无法训练的情况,估计是因为这个了。
改了之后就可以完成单正样本框选了。
上个图纪念一下,初步可以开始分清物体和背景了,不要在意bird分类,用来代表正样本的。。
开心