背景
最近在训练yolo,得到的best模型结构,输出是两个yolo,那推理时候是使用哪个output呢?
yolov3原版是3个yolo节点的
yolo的predict代码逻辑
执行的命令如下:
darknet.exe detector test cfg/coco.data yolov4.cfg yolov4.weights -ext_output dog.jpg
结合darknet.c源码可以看出在预测时候,从最后往前找layer type,第一个不是COST类型,就break跳出循环,取出结果
根据代码看,darknet预测时候就是拿的第一个遇到不是cost的输出结果,不是COST类型的layer第一个i就是yolo
int main(int argc, char **argv)
|--void run_detector(int argc, char **argv)
|--|--void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh,float hier_thresh, int dont_show, int ext_output, int save_labels, char *outfile, int letter_box, int benchmark_layers)
|--|--|--float *network_predict(network net, float *input)
|--|--|--|--void forward_network(network net, network_state state)
void forward_network(network net, network_state state)
{
state.workspace = net.workspace;
int i;
for(i = 0; i < net.n; ++i){
state.index = i;
layer l = net.layers[i];
if(l.delta && state.train){
scal_cpu(l.outputs * l.batch, 0, l.delta, 1);
}
//double time = get_time_point();
l.forward(l, state);
//printf("%d - Predicted in %lf milli-seconds.\n", i, ((double)get_time_point() - time) / 1000);
state.input = l.output;
/*
float avg_val = 0;
int k;
for (k = 0; k < l.outputs; ++k) avg_val += l.output[k];
printf(" i: %d - avg_val = %f \n", i, avg_val / l.outputs);
*/
}
}
|--|--|--|--float *get_network_output(network net)
float *get_network_output(network net)
{
#ifdef GPU
if (gpu_index >= 0) return get_network_output_gpu(net);
#endif
int i;
for(i = net.n-1; i > 0; --i) if(net.layers[i].type != COST) break;
return net.layers[i].output;
}
根据下面nms可以看出yolo的输出layer一般是YOLO、GAUSSIAN_YOLO、REGION三种
int num_detections(network *net, float thresh)
{
int i;
int s = 0;
for (i = 0; i < net->n; ++i) {
layer l = net->layers[i];
if (l.type == YOLO) {
s += yolo_num_detections(l, thresh);
}
if (l.type == GAUSSIAN_YOLO) {
s += gaussian_yolo_num_detections(l, thresh);
}
if (l.type == DETECTION || l.type == REGION) {
s += l.w*l.h*l.n;
}
}
return s;
}
ncnn推理
根据yolo的predict逻辑,就可以在ncnn上指定output进行推理
可以指定任意blob为output节点,然后net根据指定的blob递归往前回溯,找到计算路经后进行推理
int Extractor::input(const char* blob_name, const Mat& in)
{
}
int NetPrivate::forward_layer(int layer_index, std::vector<Mat>& blob_mats, std::vector<VkMat>& blob_mats_gpu, std::vector<VkImageMat>& blob_mats_gpu_image, VkCompute& cmd, const Option& opt) const
{
const Layer* layer = layers[layer_index];
// NCNN_LOGE("forward_layer %d %d %s", layer->support_vulkan, layer_index, layer->name.c_str());
bool cmd_submit_and_wait = false;
bool image_allocation_failed = false;
IMAGE_ALLOCATION_FAILED:
if (image_allocation_failed)
{
NCNN_LOGE("forward_layer %d %s image allocation failed, fallback to cpu", layer_index, layer->name.c_str());
}
if (layer->one_blob_only)
{
// load bottom blob
int bottom_blob_index = layer->bottoms[0];
if (blob_mats_gpu_image[bottom_blob_index].dims == 0 && blob_mats_gpu[bottom_blob_index].dims == 0 && blob_mats[bottom_blob_index].dims == 0)
{
int ret = forward_layer(blobs[bottom_blob_index].producer, blob_mats, blob_mats_gpu, blob_mats_gpu_image, cmd, opt);
if (ret != 0)
return ret;
}
yolo层(YOLOV3新增的层),各字段含义如下:
[yolo]
mask = 0,1,2 #当前属于第几个预选框
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326 #预选框, 将样本通过k-means算法计算出来的值
classes=80 #网络需要识别的物体种类数
num=9 #预选框的个数,即anchors总数
jitter=.3 #通过抖动增加噪声来抑制过拟合
ignore_thresh = .7
truth_thresh = 1
random=1 #设置为0,表示关闭多尺度训练(显存小可以设置0)
问题
为啥darknet预测时候只取了一个layer为yolo的output?是不是应该取三个?
ncnn上指定其中任意一个yolo的blob为output,人感觉效果差异不大
yolo转ncnn中split/eltwise层的含义
yolo中激活函数:
很多层里面有 activation 这一项,这是激活函数,我看到的配置文件里面最常用的就3个:
LINEAR:啥都不干
RELU :值 > 0时保持不变,小于0时置0
LEAKY :值 > 0时保持不变,小于0时值 * 0.1 (类似于caffe的prelu层)
卷积层:
[convolutional]
filters=96 # 输出blob通道数
size=11 # 卷积核尺寸 (长宽相同),等价于caffe里面的kernel_w, kernel_h
stride=4 # 移动步长
pad=0 # 是否在边缘补 0 最终的padding为size/2(当pad = 1)
activation=relu # Relu 激活函数
shortcut 层:
类似于caffe 的 eltwise 层(add),也就是把两个c h w都相同的两个层相加成一个相同c h w的层。
yolo转为ncnn时,shortcut对应eltwise
[shortcut]
from=-3 #和往前数第三个层相加
activation=linear
其意义为连接两个输出通道数和分辨率相同的输出,输出的通道数和分辨率也与输入相同。构建没有降采样的残差网络。v3中的降采样通过单独的一层卷积层实现,与原始残差网络的降采样不同。因此要注意连接时分辨率和通道数要相同。貌似ALexeyAB版本的darknet可以连接不同分辨率的层,不是很确定?
route 层:
route layer层主要是把对应的层连接在一起,在darknet 网络结构中,要求输入层对应的width、height必须相等,如果不相等,则把route layer层的输出w,h,c都设置为0。例如输入层1:26*26*256 输入层2:26*26*128 则route layer输出为:26*26*(256+128) 它具有可以具有一个或两个值的属性层。当属性只有一个值时,它会输出由该值索引的网络层的特征图。类似于caffe的concat层。
[route]
layers = -1, 61
upsample 层:
上采样,功能类似最邻近差值缩放算法
[upsample]
stride=2
关于shortcut操作
shortcut就是相当于eltwise操作,但这里有一个问题是,efficientDet-B0的结构中存在维度不相同的特征相加的情况
layer filters size/strd(dil) input output
90 conv 576 1 x 1/ 1 60 x 34 x 112 -> 60 x 34 x 576 0.263 BF
....
....
139 upsample 2x 30 x 17 x 128 -> 60 x 34 x 128
140 Shortcut Layer: 90, wt = 0, wn = 0, outputs: 60 x 34 x 128 0.000 BF
比如上面显示的140层是将90层和139层的特征相加,但是我们可以看到两个特征的维度分别为:60*34*576和60*34*128,而且最后的输出为60*34*128。我去看了darknet的源码,是根据输出的维度,来判断需要舍弃的特征参数,这一层中就是将90层的60*34*128特征与139层的60*34*128特征进行eltwise操作。
在caffe中没有这样的层,我又不想自己写,于是,我们可以利用split layer的操作将90层的输出特征,分为60*34*0到60*34*128和60*34*129到60*34*576两个部分。【这里要注意,因为我们只用到了split操作后的第一部分特征60*34*0到60*34*128,另一部分就舍弃了,会影响最后的caffe使用,所以到时要修改caffe-yolov3的detecnet.cpp文件】
elif block['type'] == 'shortcut':
if(int(block['from'])>0):
"""
还是讲一下原理吧:
darknet中的shuortcut有一个参数from,表示除了前一层外,还接受哪一层的特征;
比如刚刚讲的140层的from值就为90,【也可以用-50代替90】
那么这里为什么from参数>0就split呢?
因为比较巧合,effi_B0.cfg中,进行shortcut操作的层,
只要两个输入层特征的维度相同,他们的from值就为负数,
两个输入层特征维度不同,from都是用正数表示的
所以刚好利用这个规律,区分shortcut的操作,
当然如果cfg结构参数有变化就要视情况而定了
"""
#添加split层
prev_layer_id1=int(block['from'])+1
slice_layer = OrderedDict()
slice_layer['bottom']=topnames[prev_layer_id1]
slice_layer['name'] = 'layer%d-slice' % layer_id
top1=slice_layer['name']+'_1'
top2=slice_layer['name']+'_2'
slice_layer['top']=[top1,top2]
#slice_layer['top']=top1
slice_layer['type']='Slice'
slice_param=OrderedDict()
slice_param['axis']='1'
slice_param['slice_point']='128'
slice_layer['slice_param']=slice_param
layers.append(slice_layer)
bottom1 = top1
else:
prev_layer_id1 = layer_id + int(block['from'])
bottom1 = topnames[prev_layer_id1]
#后面是一样的eltwise层基本操作,不做修改
prev_layer_id2 = layer_id - 1
#print('^^^^^^^^^^^^^^^^^^^^^^^^^^^')
#print('topnames:',topnames)
#print(layer_id,prev_layer_id1,prev_layer_id2)
bottom2= topnames[prev_layer_id2]
shortcut_layer = OrderedDict()
shortcut_layer['bottom'] = [bottom1, bottom2]
if block.has_key('name'):
shortcut_layer['top'] = block['name']
shortcut_layer['name'] = block['name']
else:
shortcut_layer['top'] = 'layer%d-shortcut' % layer_id
shortcut_layer['name'] = 'layer%d-shortcut' % layer_id
shortcut_layer['type'] = 'Eltwise'
eltwise_param = OrderedDict()
eltwise_param['operation'] = 'SUM'
shortcut_layer['eltwise_param'] = eltwise_param
layers.append(shortcut_layer)
bottom = shortcut_layer['top']
if block['activation'] != 'linear':
relu_layer = OrderedDict()
relu_layer['bottom'] = bottom
relu_layer['top'] = bottom
if block.has_key('name'):
relu_layer['name'] = '%s-act' % block['name']
else:
relu_layer['name'] = 'layer%d-act' % layer_id
relu_layer['type'] = 'ReLU'
if block['activation'] == 'leaky':
relu_param = OrderedDict()
relu_param['negative_slope'] = '0.1'
relu_layer['relu_param'] = relu_param
layers.append(relu_layer)
topnames[layer_id] = bottom
layer_id = layer_id + 1
yolo cfg解析
本文主要说一下yolo系列中的cfg文件,如何根据cfg文件快速了解yolo系列的网络结构。这里以yolov4.cfg文件来说明下。
1、Net 层
[net]
batch=64
subdivisions=8
#这里的batch和subdivisions表示一次性加载64张图片到内存,分8次完成前向传播,每次8张
#经过64张图片的前向传播后,完成一次反向传播及更新
#Training
#width=512
#height=512
width=608
height=608
channels=3
#输入图片的宽度和高度及通道数
momentum=0.949
#momentum动量参数影响着梯度下降到最优值得速度。
decay=0.0005
#权重衰减正则项,防止过拟合。
angle=0
#数据增强,设置旋转角度
saturation = 1.5
#饱和度
exposure = 1.5
#曝光度
hue=.1
#色调
learning_rate=0.0013
#学习率
burn_in=1000
max_batches = 500500
policy=steps
steps=400000,450000
#在达到 40000、45000 的时候将学习率乘以对应的scales值
scales=.1,.1
#cutmix=1
#Mixup:将随机的两张样本按比例混合,分类的结果按比例分配;
#Cutout:随机的将样本中的部分区域cut掉,并且填充0像素值,分类的结果不变;
#CutMix:就是将一部分区域cut掉但不填充0像素而是随机填充训练集中的其他数据的区域像素值,分类结果按一定的比例分配
mosaic=1
#使用mosaic数据增强
2、卷积层
[convolutional]
batch_normalize=1
#是否进行BN操作,当进行BN操作时,不需要设置偏置bias
filters=32
#滤波器数量,即输出特征图数量
size=3
#卷积核大小
stride=1
#卷积运算步长
pad=1
#如果pad=0,padding 由 padding参数指定。
#如果pad=1,padding大小为size/2,而不是真的填充为1
activation=mish
#激活函数为mish,当然也有其它的如relu,leaky,linear等
#Mish:一个新的state-of-the-art激活函数,ReLU的继任者,Mish=x * tanh(ln(1+e^x))
https://zhuanlan.zhihu.com/p/84418420
3、下采样层
# Downsample
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
#这里pad=1,所以实际的padding=size/2=3/2=1
activation=mish
输出特征图尺寸大小计算公式为:
其中M为输出尺寸大小,N为输入特征图尺寸大小。
例如,当N=416时,size=3,stride=2,pad=1,所以padding=size/2=1,得M=208。
4、shortcut和route层
[route]
layers = -2
#当属性layers只有一个值时,它会输出由该值索引的网络层的特征图。
#本例中就是提取从当前层倒数第 2 层输出特征图
通道(Channel),也有叫特征图(feature map)的。
https://www.cnblogs.com/lfri/p/10491009.html
[route]
layers = -1,-7
#当属性layers有两个值,就是将上一层和从当前层倒数第7层进行融合,大于两个值同理。
#即沿深度的维度连接,这就要求feature map大小是一致的。
[shortcut]
from=-3
activation=linear
#shortcut 操作是类似 ResNet 的跨层连接,参数 from 是 −3,
#意思是 shortcut 的输出是当前层与先前的倒数第三层相加而得到,通俗来讲就是 add 操作
#注意add操作和融合操作区别
5、上采样及池化层
[upsample]
stride=2
#上采样通过线性插值实现的。
### SPP ###
#这里以SPP结构来说明
[maxpool]
stride=1
size=5
[route]
layers=-2
[maxpool]
stride=1
size=9
[route]
layers=-4
[maxpool]
stride=1
size=13
[route]
layers=-1,-3,-5,-6
#以上每个maxpool的padding=size/2
### End SPP ###
在一般的CNN网络结构中,最后的分类层通常是由全连接组成,而全连接有个特点,那就是它的特征数是固定的,这就导致了图片在输入网络的时候,大小必须是固定的,但是在实际情况中,图片大小是多种多样的,如果不能满足网络的输入,图片将无法在网络中进行前向运算,所以为了得到固定尺寸的图片,必须对图片进行裁剪或者变形拉伸等,这样就很可能会导致图像失真,从而影响最终的精度,而我们希望网络能够保持原图大小的输入,得到最大的精度。
在这里插入图片描述
SPP全称为Spatial Pyramid Pooling(空间金字塔池化结构),它是由微软研究院的何凯明大神提出,主要是为了解决两个问题:
有效避免了对图像区域剪裁、缩放操作导致的图像失真等问题;
解决了卷积神经网络对图像重复特征提取的问题,大大提高了产生候选框的速度,且节省了计算成本
所以说,YOLOv3-SPP版本实际上只是增加了SPP 模块,该模块借鉴了空间金字塔的思想,通过SPP模块实现了局部特征和全局特征,这也是为什么SPP模块中最大的池化核大小要尽可能的接近或者等于需要池化的特征图的大小,特征图经过局部特征与全局特征相融合后,丰富了特征图的表达能力,有利于待检测图像中目标大小差异较大的情况,尤其是对于YOLOv3这种复杂的多目标检测,所以对检测的精度上有了很大的提升。
https://blog.csdn.net/qq_39056987/article/details/104327638
6、YOLO层
[yolo]
mask = 0,1,2
#对应的anchors索引值
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
#根据训练数据集聚类得到的9个anchor框大小
classes=80
#类别个数
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.2
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5
droput
[dropout]
probability=.15
在前向传播的时候,让某个神经元的激活值以一定的概率p停止工作,这样可以使模型泛化性更强,因为它不会太依赖某些局部的特征
7、实例说明
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=mish
# Downsample
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=mish
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish
[route]
layers = -2
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish
[convolutional]
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=mish
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=mish
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish
[route]
layers = -1,-7
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=mish
注:以上cfg文件对应于YOLOV4的CSPDarknet53中的第一个模块,可视化结构如下。
图1 CSPDarknet53结构局部示意图
因此,通过对yolo系列的每一个cfg文件解读,我们可以很快了解其网络结构。
参考链接:
https://blog.csdn.net/weixin_38715903/article/details/106124518