这篇文章主要帮助复习Yolo3, 需要你首先对Yolo3有一定了解, 至少看过一边它.
Yolo3是一个端到端的模型, 为了方便理解, 我把它分为两个部分, 分别为特征提取网络和定位网络. 在特征提取网络中, 它使用了Darknet-53, 之所以有53这个数字, 是因为它有53层卷积. 在定位网络中, 它会输出基于图像金字塔结构的三个尺度定位结果.
特征提取网络:
我们先来看看这个Darknet-53, 它的特点是有53层卷积(除去最后一个FC总共52个卷积用于当做主体网络)和带有残差连接. 下图很好的可视化了该网络.
这里有两个细节需要注意
- 当Darknet-53中的DBL的strides为1的时候, 默认padding都是‘same’. (见上图中的Darknetconv2D_BN_leaky和Res_uinit模块)
- 当Darknet-53中的DBl的strides为2的时候, 默认padding都是valid, 但是在这之前会进行一个 zeros padding的步骤, 就是在左边和上面各加一行都为0的padding, 来保证经过卷积后, 长宽各缩小一倍. (见图中的Resblock_body)
定位网络:
在神经网络中, 随着网络层数的加深, 该层的视野区域就越大, 但信息也丢失的越多, 所以为了平衡在不同大小的标检测上的效果, Yolo3使用了基于图像金字塔结构的 多尺度 特征输出, 也就是Darknet-53中较浅的层数输出一组特征, Darknet-53中间的层数输出一组特征, Darknet-53最深的层数地方输出一组特征. 然后这三组特征, 会再经过神经网络的处理, 最后输出三组不同尺度的定位目标结果. 下图很好的展示了定位部分网络的结构
这里有几个细节需要注意:
- y1, y2, y3分别对应了不同尺度的检测结果, 比如 y1中的 13 * 13 * 255, 表示把图片平分为 13 * 13个小格子, 255 表示了每个格子的预测目标结果, yolo3设定的是每个网格单元预测3个box,所以每个box需要有(x, y, w, h, confidence)五个基本参数,然后还要有80个类别的概率。所以3×(5 + 80) = 255
- y1, y2, y3 的输出都是没有经过归一化的(它们只是卷积之后的结果), 所以 [x, y, w, h, confidence, classes] 需要归一化, [x, y, confidence和classes] 用 sigmoid 归一化到 0 ~ 1, 同时[x, y] 需要再加上偏移量Cx(横向第几个格子)和Cy(竖向第几个格子), [w, h] 用prior anchors box来归一化 (width of prior anchor box* np.exp(w) 和 height of prior anchor box* np.exp(h)). w和h不用 sigmoid的原因有两点: (1): 它们有可能比 prior anchors box 大, 也就是比例大于1, 而sigmod的范围是 0 ~ 1. (2): yolo3想要建立起预测结果与prior anchors box的关系.
- Prior Anchors box 的大小定义见下图, 是使用K-Means在训练集中求得的.
- 目标定位的步骤如下. (1) 计算每个boudingbox中得分最高的confidences of class [np.max(confidence * possibilities of each class)] (2) 过滤所有confidence过低的boudingbox (3)对剩下的boudingbox进行Non-max suppression(基于每个class)
下面是预测结果的代码
Yolo3网络框架
# based on https://github.com/experiencor/keras-yolo3
import struct
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
def _conv_block(inp, convs, skip=True):
x = inp
count = 0
for conv in convs:
if count == (len(convs) - 2) and skip:
skip_connection = x
count += 1
if conv['stride'] > 1: x = ZeroPadding2D(((1,0),(1,0)))(x) # If tuple of 2 tuples of 2 ints: interpreted as ((top_pad, bottom_pad), (left_pad, right_pad))
x = Conv2D(conv['filter'],
conv['kernel'],
strides=conv['stride'],
padding='valid' if conv['stride'] > 1 else 'same', # peculiar padding as darknet prefer left and top
name='conv_' + str(conv['layer_idx']),
use_bias=False if conv['bnorm'] else True)(x)
if conv['bnorm']: x = BatchNormalization(epsilon=0.001, name='bnorm_' + str(conv['layer_idx']))(x)
if conv['leaky']: x = LeakyReLU(alpha=0.1, name='leaky_' + str(conv['layer_idx']))(x)
return add([skip_connection, x]) if skip else x
def make_yolov3_model():
input_image = Input(shape=(None, None, 3))
# Layer 0 => 4
x = _conv_block(input_image, [{
'filter': 32, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 0},
{
'filter': 64, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 1},
{
'filter': 32, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 2},
{
'filter': 64, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 3}])
# Layer 5 => 8
x = _conv_block(x, [{
'filter': 128, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 5},
{
'filter': 64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 6},
{
'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 7}])
# Layer 9 => 11
x = _conv_block(x, [{
'filter': 64, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 9},
{
'filter': 128, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 10}])
# Layer 12 => 15
x = _conv_block(x, [{
'filter': 256, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 12},
{
'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 13},
{
'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 14}])
# Layer 16 => 36
for i in range(7):
x = _conv_block(x, [{
'filter': 128, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 16+i*3},
{
'filter': 256, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 17+i*3}])
skip_36 = x
# Layer 37 => 40
x = _conv_block(x, [{
'filter': 512, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 37},
{
'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 38},
{
'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 39}])
# Layer 41 => 61
for i in range(7):
x = _conv_block(x, [{
'filter': 256, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 41+i*3},
{
'filter': 512, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 42+i*3}])
skip_61 = x
# Layer 62 => 65
x = _conv_block(x, [{
'filter': 1024, 'kernel': 3, 'stride': 2, 'bnorm': True, 'leaky': True, 'layer_idx': 62},
{
'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 63},
{
'filter': 1024, 'kernel': 3, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 64}])
# Layer 66 => 74
for i in range(3):
x = _conv_block(x, [{
'filter': 512, 'kernel': 1, 'stride': 1, 'bnorm': True, 'leaky': True, 'layer_idx': 66+i*3},
{
'filter': 1024, 'kernel': 3,