LPCV20比赛准备 / Object Detection简述

看看这一年目标检测领域又背着我发展了多少(雾)

Posted by tianchen on May 7, 2020

比赛TODO

需要做的东西还真不少

  • 目前主要需要做的是去完成、Pytorch - TF - Tflite一套工具链
    1. 编译安装tf与bazel
    2. tflite的使用
    3. 模型转换工具
  • 还有定点上不知道有没有什么空间

  • TODO:
    1. 编译tf (Finished)
    2. 安装android studio
      • 编译apk
    3. 跑起来EfficientDet
      • COCO Dataset
    4. 正常走完android开发的块速流程

Tflite工具链的安装流程

充满血与泪的一周多,tf这东西真不是人用的,主要内容填写在Git issue当中

  1. PIP安装TF 1.1 tf-2.1版本在import的时候出现问题 1.2 Downgrade 2 tensorflow 2.0 via pip install tensorflow-gpu==2.0.0
  • 其中的prequisite部分指引向
  1. 从头开始编译TF
    • 由于TFlite需要build from source
    • 主要参考了tf官方的版本
    • 主要遇到的阻力是网络问题
      • 由于遇到了需要下墙外的资源的问题,由于bazel需要通过建立localhost,所以直接用proxychains搭配ss不能正常安装
      • 首先给git配置代理 git config --global http.proxy socks5://127.0.0.1:10808
        • 然而到了之后这个还是不起作用,反而需要改动
        • (另外一种改变git config的方法是在~/.gitconfig)
      • 我们实际需要的是http代理,而socks并不能提供该种代理,所以使用了privoxy
        • Privoxy的配置教程
        • 首先sudo apt-get install privoxy
        • 编辑 sudo vim /etc/privoxy/config
          • 搜索listen-address保证listen-address 127.0.0.1:8118存在,将来的http代理将在8118端口
          • #forward-socks5t / 127.0.0.1:1080 .接触注释(注意结尾的“.”)
        • 重启Privoxy
          • systemctl restart privoxy
          • systemctl enable privoxy
        • ~/.bashrc中添加
          • export http_proxy=http://127.0.0.1:8118
          • export https_proxy=https://127.0.0.1:8118
      • curl www.google.com 来测试是否成功
      • 注意改完之后git的config也要改过来,git config --global https.proxy https://127.0.0.1:8118 1.1 开始安装一些初步的
      • sudo apt install python-dev python-pip
      • pip install -U --user pip six numpy wheel setuptools mock 'future>=0.17.1'
      • pip install -U --user keras_applications --no-deps
      • pip install -U --user keras_preprocessing --no-deps 1.2 安装bazel
      • 查看需要的bazel版本,查看tf源码目录下的configure.py
      • Release界面直接下载.sh
      • 运行xxx.sh --user
      • 在bashrc中添加export PATH="$PATH:$HOME/bin" 1.3 编译Source Code
      • git clone https://github.com/tensorflow/tensorflow.git
      • git checkout r1.15
      • ./configure
        • 请完成sec2.1再调用此行
        • 这里实际上需要葱figure好android再执行
      • bazel build --config=v1 --config=opt //tensorflow/tools/pip_package:build_pip_package
      • (希望确实是能够work的)
    • ./bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg
    • pip install /tmp/tensorflow_pkg/tensorflow-version-tags.whl
  • 我不知道是否需要手动安装,但是我现在的jdk版本是11.0.7,应该可以直接apt安装获得
  1. 配置android
    • 下载ndk
    • 通过android studio安装sdk
    • 在1.3的./configure中指定SDK/NDK Path
    • 讲道理下面的指令就可以编译了
      • bazel build -c opt --fat_apk_cpu=x86,x86_64,arm64-v8a,armeabi-v7a \ --host_crosstool_top=@bazel_tools//tools/cpp:toolchain \ //tensorflow/lite/java:tensorflow-lite
    • 将在bazel-bin/tensorflow/lite/java/生成了aar文件,将其复制到android项目中并修改build.gradle
    • (其实这一步我真的不清楚到底如何起效果,而且看上去好像是删除了两行android依赖才完成的最后的编译,所以成功性存疑)
  2. Follow Ovic Guide 然后测试一系列东西
    • 从一开始的validate,到生成最后的apk
      • curl -L https://storage.googleapis.com/download.tensorflow.org/data/ovic_2019_04_30.zip -o /tmp/ovic.zip
        • 用来测试的.lite文件可以在这个文件夹中获取
      • 如果要利用自己的模型,需要从tf的ckpt开始
        1. 利用4步骤中完成的export_ssd_graph来从ckpt文件中获取frozen_graph.pb文件
        2. 利用tf编译产生的bazel-bin中的tflite-convert从上一步的pb文件中生成.lite文件用作提交与测试
  3. 安装tensorflow-models
  4. 导出以及撰写pipeline.cfg
    • Refer to the object_detection/export_tflite_ssd_lib.py
    • Official Guide

Tensorflow

需要梳理一些TF的basic,不然有点寸步难行

Cheet-Sheet

  • check tensorflow version tf.__version__()
  • check tensorflow is_cuda tf.test.is_built_with_cuda()

Graph

  • Example

a = tf.placeholders(tf.float32, [])
b = tf.constant(1.0)
c = tf.add(a,b)
with tf.Session() as session:
  print(session.run(c, feed_dict={a:1.0}))  

  • Tf会自动将计算转化为图上的节点,系统会维持一个默认的计算图
    • x.graph可以获取当前tensor所在的计算图
    • tf.get_default_graph()获取默认计算图
    • tf.Graph()手动生成新图,在不同图中的同一变量,操作和值也是不一致的
  • 在某个计算图中,collection 用来整合不同类别的资源
    • g.get_all_collection_keys()
    • tf.add_to_collection()
  • 一般图的输入用一个placeholder来定义 x=tf.placeholder(tf.float32, shape=(1,300,300,3),name="xxx")
    • 然后在session运行的时候调用 sess.run(y, feed_dict = {x: [[[...]]]})
  • graph.get_operations() 获得各种节点

GraphDef

  • gd = tf.get_default_graph().as_graph_def()
  • 打印出来是一个string的样子
  • 转化为二进制图并存储pb
    • gd.SerializeToString() 然后用wb的模式写入pb文件
  • gd.node[0] 可以看作一个list来用
    • 再往里有属性 name / op / attr
    • 其中attr是一个dict
      • value(就是一个tensor) / dtype
  • 获取所有key
for n in gd.node:
    print(n.name)

Tensor

  • Tf的Tensor中没有真实的值,只是保存了如何获得这些数据的过程,主要是张量的结构
  • 默认dtype是tf.float32
  • 张量的维度是不能改变的,如果需要强制替换到某个维度变换的向量,必须指定validateshape=False
    • 调用tf.assignn(x1,x2,validate_shape=False)

Variable

可以保存和更新的变量

  • tf.Variable()变量的声明,本身是一个运算(OP),该op的结果是一个Tensor(张量)
    • variable可看成某一种特殊的张量
  • 变量会自动加入图,可以用tf.global_variables()获得所有的参数
  • 可以用trainable属性来标识是否会被训练,用tf.trainable_variables来获取

Constants

一旦写入,就不能再改变

  • tf.constant(1.0, name="a")

PlaceHolder

  • tf.placeholder(dtype(tf.float32), shape, name)
  • run的时候用sess.run(output, feed_dict={b:1.0}
    • feed_dict当中填入的是对应的变量名字,而不是name
    • 同理sess run的也是

Session

TF的运行模型

  • 常在一个sess scope中进行执行
    • with tf.Session() as sess:
  • sess=tf.Session(); with sess.as_default():
    • 通过将session register为default,调用eval等函数的时候就可以不用在参数指定x.eval(session=sess)
  • sess = tf.InteractiveSession() - 通过交互式环境下直接构建成默认会话,省去了注册默认会话的过程
  • 在session内部,tf.Tensor.Eval()函数可以用来获得某个tensor的取值
  • 一般的NN训练流程
    • 主干图graph,输入placeholder,输出logit
    • 输出logit套一个损失函数得到loss
    • 定义trainer_step - train_step = tf.train.AdamOptimizer(learning_rate).minimize(cross_entropy)
    • 在主干循环(iter)中调用sess.run(train_step)
    • 在一开始可能需要有一个init
      • init_step = tf.global_variables_initializer(); sess.run(init_step)
    • 其中需要打印出来的变量,sess.run(loss, feed_dict={})并且打印出来

Model

  • 定义网络结构的几种方式
from tensorflow.keras import Model

class MyNet(Model):
    def __init__(self):
        super(MyNet,self).__init__()
        self.conv1 = Conv2D(32,1,activation="relu")
        self.flatten = Flatten()
        self.d1 = Dense(128,activation="relu")
        self.d2 = Dense(10)
    def call(self, x):
        x = self.conv1(x)
        x = self.flatten(x)
        x = self.d1(x)
        x = self.d2(x)
        return x

net=MyNet()
net.summary()

Ckpt-模型存储

  • tf的模型类型:
    • 一个接地气教程
    • 一个StackOverflow
    • 自己参考写并测试了一个load脚本好像是能用的load.py
    • ckpt
      • .ckpt是老版本的ckpt的输出(B4 1.12),现在的输出由meta/index/data/checkpoint文件组成
        • meta-保留了完整的图结构,由saver = tf.train.import_meta_graph()
        • data/index-保留了所有的参数 saver.restore(sess, tf.train.latest_checkpoint("./"))
          • data中包含了所有变量
          • index中描述了variable中的key与value的对应关系
      • Save -
        • saver产生一个文件夹中的4个文件
      • Load -
        • 这之后可以用tf.get_default_graph().get_tensor_by_name()
        • 或者打印变量名 sess.run('w1:0')
        • 如果希望读出所有的内容
          • from tensorflow.python import pywrap_tensorflow
          • reader = pywrap_tensorflow.NewCheckpointReader(checkpoint_path)
            • 注意这里的ckpt_path,如果模型的名字是model.meta,那么这里就是model
          • var_to_shape_map = reader.get_variable_to_shape_map()
          • for key in var_to_shape_map:
            • key是各个变量的名字
            • value是各个变量的shape
            • 提取值的话用 reader.get_tensor(key)
              • 取出的东西是一个np-array
      • tf.train.Saver()调用save()生成的,不包含图的结构
      • saver.restore(session, ckpt_path)来重构计算图
    • pb文件(GraphDef)(经过了protobuf),包括了图以及内部的参数
      • 是二进制文件不可读,pb也支持文本形式(.pbtxt)但是包含文本文件大
      • 从中构建计算图
        • graph=tf.Graph()
        • graphdef = tf.GraphDef()
        • with open("*.pb","rb") as f: graph_def.ParseFromString(f.read())
      • 作为与训练模型的pb文件是从ckpt中freeze而来,包含了所有参数
    • saved_model (目前还没有遇到过)
      • 包含了graphdef和ckpt
    • 对于PB对象,于其用tf-board进行graph可视化,可以直接使用Netron来进行快捷可视化图
  • 目前模型的输出: (也就是detect所给出的结果)
    • raw_image (1,300,300,3)
    • feature_maps
    • Anchors - concat:0 (1917,4)
    • Final-anchors - tile:0 (1,1917,4)
    • Box-encoding - squeeze:0 (1,1917,4)
    • Class-predictions - concat:0 (1,1917,91)
      • 90 classes + 1 background
      • 后续交给 score_conversion_fn操作
    • box-encoding以及class-prediction抽出来做一个identity加上raw-output的scope

TF-lite

杂项 Mixed

  • 直接import XXX会在项目的根目录下寻找,所以找不到
    • 如果改成模块化的import from . import utils相对导入,在当前.py文件的目录下
      • .的就是相对导入,是从当前py文件的路径下
    • py3默认绝对导入
      • 是从执行文件夹下

Object Detection 综述

  • 从去年这个时候做SSD-FPGA之后,就没怎么关注过这一块了,在DL中一年不关注一个领域的发展可见一斑(特别还是Detection这么火的一个领域),来补课了
  • 文章来自于Arxiv又看的zhihu的翻译版,我自己再夹杂一些私货,算是二次咀嚼了,反刍(不是)

Metrics

  • Precison = True Positive/All Positve
    • 最后判断为Positive的有多少是正确的
    • 放心度 模型判断出来正确我用起来多放心,可以很放心,但是过度谨慎(recall度 )
  • Recall = True Positive/(TP+FN)->All Positive(GT) -
    • GT中有Positive多少被判断出来了
    • 敏锐度 模型能够把客观正确的判断出来多少,可以很敏锐,但是很粗心(Precision低)
  • AP - Average Precision
    • 不同Recall下的Precision的Mean
    • 详细计算方法参考这个教程
      • 分别计算PR(这里计算的是框子的数目,不是类别)并画出PR曲线
      • PR曲线下的积分面积就是AP
  • mAp - meanAP - AP是基于某一类的,mAp就是求AP关于类的平均

Methods

Faster-RCNN 2015

  • 参考解读文章
  • 祖宗
  • CNN end2end训练,速度几乎实时
    • 一次性完成区域提取,特征提取bounding box和回归
  • 用RPN(Region Proposal Network)以“Anchor”的概念,代替了Selective Search
    • 以Feature Map(经过降采样之后不大的)上的每一个点作为中心,在原图映射为9个anchor(3个长宽比,3个尺度)
    • 这里RPN就是分别对Feature Map的每一个像素点的256维度的特征,做1x1x18的卷积得到每个AnchorBox对应的前景后景置信度(9x2)以及每个BBox的位置xywh(9x4) (讲道理每个锚点对应会原图都会有一个特定位置,RPN给出的Proposal Box也应该在那附近 - ?怎么实现这一点或者说是这个所谓的anchor映射到原图,并不是一个直接的位置映射(FeatureMap左上角对应到原图的左上角)或者说,Anchor到原图的映射,正是从每个Anchor中训练出来的xywh这个过程,所以所谓的“锚点”其实是抽象的点)
    • 以IOU作为前景后景的打分标准
  • 经过RPN,我们将原图中产生的anchor box返回到Feature Map,得到了一些Proposal,然后做一个ROI Pooling(将抠出来的Proposal转化为统一大小)
    • 比如这里将7*5的Proposal做MaxPooling到2x2
  • Loss是分类损失以及位置回归误差之和

R-FCN 2016

  • Full Conv + Position Sensitive
  • 全卷积,所有参数在整张图片上是共享的
  • Faster-RNN在ROI-Pooling之前是严格卷积,具备平移不变性,但是之后就不存在了
    • RFCN采用Position Sensitive Score Map
  • Faster-RCNN的RPN之后的部分需要独立地对每一个Region做
    • RFCN想要尽量共享参数
    • 提取出Roi这一步是一样的,但是Faster-RCNN对每个Roi(Region Proposal)独立处理了,但是RFCN不一样
  • 细节:
    1. 首先经过卷积层,最后一个FeatureMap有三个分支 1.1 RPN的操作获取Region 1.2 获取Position-Sensitive-Score-Map (KxKx(C+1))的用来分类 1.2 获取Position-Sensitive-Score-Map (KxKx(4))的用来定位
    2. 对Position-Sensitive-Score-Map做Roi-Pooling(Average Pooling)
  • ?How Position Senstive
  • 即使Roi偏了,也能正常识别

Yolo(You Only Look Once) 2015

  • 简单暴力所有东西拿CNN硬干
  • 第一个1-Stage的网络,抛弃了区域提取+验证的2-stage思想,直接将原图分为S*S的grid cell,每个里面塞B个Bounding Box,推测C类,每个BBox预测5个value(xywh+conf)最后需要预测的Tensor为 S*S*(B*5+C)
  • How 2 Get Box 每一个BBox都会对应一个confidence,等价于GT与该Bbox的IOU(如果这个Grid Cell里面没有Obj则conf=0,判断方式是gtbox的中心是否落在这个gridcell里面)然后Bbox的conf再和20个类别所得的置信度相乘,获得每一类的最终置信度,并经过一个NMS(防止一个Obj对应着太多的Box)
  • Loss = 定位误差 + IOU误差 + 分类误差
  • 速度极快,精度也随着改进跟上了
    • 引入Res,引入Anchor,加入BN
  • 发展历史:
    • Yolo v1:
      • Features: 1. 用Grid Cell划分区域之后,对每个区域独立进行检测(分治法) 2. 完全End2End 3.应用了Leaky relu
      • Shortcomings: 1. 位置准确度不足 2,对小物体效果差(由于每个格子最多检测出一个物体) 3. Recall比2-Stage的方法小
    • Yolo V2 (Yolo9000) - 主要改善Recall以及定位精度 各种Trick拉满
      • 加入了BatchNorm
        • 🤔Why Could BN Help: 网络层数加深,整体数据的分布会越来越偏,由BN强制拉到一个比较标准的分布(不然会发生Internal Covariance Shift)
          • 本质上还是把数据拉到损失函数比较敏感(梯度比较大的地方)便于收敛
      • Anchor Box的引入
        • RPN的核心也是Anchor,通过anchor,让feature map上的点映射回到原图的点,这样需要预测的就不是直接的坐标而是偏移量了
        • Yolo V2把全连接换成了Anchor Box方式进行预测
      • 用K-means训练Bbox的Size
      • 添加了细粒度模块添加了一个PassThrough层,将相邻的特征按通道叠加而不是按空间叠加,这样让高低分辨率的特征能够串接起来
        • 这篇文章表示这种方法和Res异曲同工,我没理解
        • 🤔:Why Res Help(我感觉这个观点有点意思) Resnet通过一个shortcut feedback,学习的是f(x)-x,举一个比较极端的例子,假设f(x)=x(恒等映射)学习一个g(x) = f(x)-x=0比学习一个x要简单(而且NN的初始化一般都是0均值的)
      • MultiScale-Training 多尺度训练
    • Yolo V3 (Even Faster)
      • Deeper
      • ResNet Backbone

SSD (Single Shot Detector)

  • 引入了多目标检测
    1. 多尺度特征图(大的特征图检测小物体(分辨率更高)) - 借鉴了金字塔结构
    2. 摈弃FC,用卷积作为来检测
    3. 保留了先验框(Faster RCNN中的Anchor)为每一个单元格子都设置长宽比不同的先验框,是BBox的基准,对于每个anchor box都独立作出判别
    4. 训练中Ground Truth与Box的配准: yolo较为naive,gt的中心在那个单元格,就找(该grid cell中)与其IOU最大的bbox负责
      • 而SSD则更为“谨慎”,对于每一个gt都找出图中所有的anchor box(比前面少了当前的grid cell中)与其IOU最大的,然而这样会造成负样本过多(超多的anchorbox没有对应的gt),因而需要降低标准,对每一个anchor,如果与gt的iou大于某一阈值,也看做匹配

FPN(Feature Pyramid) CVPR 2017

  • 提出了一种新的特征提取器(在Object Detection中属于BackBone)
  • 金字塔的结构,更好适应多尺度,Pyramid体现在特征图的尺度,分为几个级别(每个级别是一个ResBlock)
  • 解决了之前的图像金字塔的计算量过大的问题
    • 原本的比较Naive,相当于是在多个Scale下分别进行预测
  • 核心思想 高低层特征融合把高层的语义传下来,同时获得具有高分辨率与高语义度的特征图,可以有利于小目标的检测
  • 如何能够结合高低尺度分辨率的特征? 把更抽象,语义信息更强的高层特征图(在更深的位置)把该层的特征横向链接(lateral connection)到前一级特征图,让高级特征得到增强(两个feature map尺度要相同-via对更深层的feature map做上采样(via直接复制,不是插值或者是反卷积),才可以利用位置信息)
    • 浅层的feature map需要用1*1Conv把channel数目也弄得和深度的一样
    • 结合的方式是直接逐像素相加

  • NN中融合特征的方法
  • 直接逐像素Add相当于先Concat再让通道共享同一个卷积核-用Add其实更节省计算量和参数
  • ADD Example
    • ResNet的Skip Connection
    • FPN中的多尺度Feature Map融合
  • Concat
    • Skip Connection其实很多时候用的都是concat - DenseNet

RetinaNet

  • 注重于one-stage为何不如2-stage精确度高的问题
    • 前景后景之间的不平衡 (背景框太多让训练跑偏)
    • 样本难以区分程度差距大 (需要花更多精力在难区分样本上)
  • 通过Focal Loss解决

TridenNet (在深海使用三叉戟)2019

  • 关注尺度变化的问题,研究不同感受野的优劣
  • 将传统conv换空洞卷积,从而改变不同分支的感受野的大小

🤔🤔🤔Thoughts

Rethink Anchor

  • anchor 的本意是“锚“(anchor box - 锚框)表示固定的参考框
  • B4anchor: 采用金字塔尺度+SlidingBox的方法去判断这个位置可不可能有对象 笨重

    这个尺度,的某个位置处,有没有我们想要的目标

  • 而anchor:首先预设置一组不同大小的框(几乎覆盖了所有位置和尺度)每个框负责与它IOU最大的对象

    这个固定的框子(Anchor Box)内部有没有我们想要的对象且偏离多远

  • 🤔:两者一个基于对象,一个基于框。
    • 个人觉得两者在思路上还是类似,Anchor所做的主要是通过对卷积之后特征图上设置“锚点”并以此为基点(通过卷积)将Proposal映射到原图上的点这一步,省去了原先SlidingBox每个Box都相当于要做特征提取(卷积)那样又慢又没有考虑到全图的信息
    • Anchor Box相比于之前的方法主要 1,减少了计算量 2.解决了长宽比
  • Anchor’s Problem:
    • Anchor的尺度和大小如何确定()
      • YoloV2聚类,训练出anchor
    • 小目标是一个难题
  • Region-Based Methods 3 Stepss
    1. 一个Backbone给出Feature Map
    2. RPN之类的网络出Region Proposal(必须对每个Region独立,参数不共享 - 体现在RPN对FeatureMap每个输出点做1x1xN的卷积) 2.5. Roi Pooling
    3. 最后的分类回归网络

Anchor Free方法

  • Anchor的缺陷
    • 需要手动支持
    • Anchor的匹配让极端尺度的判别很难
    • Anchor的数量很多的时候需要一个不平衡的问题
      • Focal Loss能一定程度上解决
  • 一开始的Yolo和DenseBox,目前的SOTA是CVPR2018的CornerNet
    • 2019年,很多基于Keypoint的Anchor Free方法逐渐被提出
      1. CornerNet
      2. CentreNet
      3. ExtremeNet

SubFileds (术业有专攻)

多尺度目标检测 MultiResolution

  • Timeline

Refs