深度神经网络dnn最近几年非常火爆。其实其算法不是新鲜事,很早就有。只是当时计算机的运算能力,不足以支持DNN网络所需要的巨大计算量,所以应用的并不广泛。随着计算机硬件技术的发展,算力有了很大的提升,利用神经网络处理数据才逐渐变得时髦,即便如此,也应该看到神经网络在训练、优化数据模型时仍然需要巨大的计算量和可观的能源消耗(电力)。

DNN并不高深,下面我们不借助于任何框架,就可以实现一个DNN案例,希望以此揭开DNN的所谓的“神秘面纱”。

27.1 用python实现DNN的代码示例

新建一个4层神经网络:输入层【1个输入特征】、第1层隐含层【4个激活项】、第2层隐含层【4个激活项】、输出层【1个输出特征】,实现样本点的回归。样本点随机产生的。

import numpy as np
import matplotlib.pyplot as plt
import sys

#在产生拟合曲线y=x^2的数据 
def generate_data():
    x = np.linspace(-2,2,100)[np.newaxis,:]
    noise = np.random.normal(0.0,0.5,size=(1,100))
    y = x**2+noise
    return x,y
 
# 定义神经网络 
class DNN():
    
    # 1 初始化神经网络(四层神经网络)
    #   input_nodes:输入节点数目(Layer1)
    #   hidden1_nodes:第一隐藏层的激活项个数(Layer2)
    #   hidden2_nodes:第二隐藏层的激活项个数(Layer3)
    #   output_nodes: 输出节点数目(Layer4)
    def __init__(self,input_nodes=1,hidden1_nodes=4,hidden2_nodes=4,output_nodes=1):
        self.input_nodes = input_nodes
        self.hidden1_nodes = hidden1_nodes
        self.hidden2_nodes = hidden2_nodes
        self.output_nodes = output_nodes
        self.build_DNN()

    # 2 构建DNN: 主要是设置各层神经网络的权重矩阵和偏置量
    def build_DNN(self):
        np.random.seed(1)
        # Layer1 parameter
        #     权重矩阵w1
        #     偏置项b1
        self.w1 = np.random.normal(0.0,0.1,size=(self.hidden1_nodes,self.input_nodes))
        self.b1 = np.zeros(shape=(self.hidden1_nodes,1))
        # Layer2 parameter
        #     权重矩阵w2
        #     偏置项b2
        self.w2 = np.random.normal(0.0,0.2,size=(self.hidden2_nodes,self.hidden1_nodes))
        self.b2 = np.ones(shape=(self.hidden2_nodes,1))
        # Layer3 parameter
        #     权重矩阵w3
        #     偏置项b3
        self.w3 = np.random.normal(0.0,0.5,size=(self.output_nodes,self.hidden2_nodes))
        self.b3 = np.zeros(shape=(self.output_nodes,1))
        
    # 3 前向传播函数
    def forwardPropagation(self,inputs):
        self.z1 = np.matmul(self.w1,inputs) + self.b1
        self.a1 = 1/(1+np.exp(-self.z1))
        self.z2 = np.matmul(self.w2,self.a1) + self.b2
        self.a2 = 1/(1+np.exp(-self.z2))
        self.z3 = np.matmul(self.w3,self.a2) + self.b3
        self.a3 = self.z3
    
    # 4 后向传播函数
    def backwardPropagation(self,da,a,a_1,w,b,last=False):
        '''
        da:当前层激活项输出误差
        a:当前层激活项输出
        a_1:上一层激活项输出误差
        w:当前层权重矩阵
        b:当前层偏置项
        '''
        # dz = da/dz
        if last:
            dz = da
        else:
            dz = a*(1-a)*da
        # dw = dz/dw
        nums = da.shape[1]
        dw = np.matmul(dz,a_1.T)/nums
        db = np.mean(dz,axis=1,keepdims=True)
        # da_1 = dz/da_1
        da_1 = np.matmul(w.T,dz)
        
        w -= 0.5*dw
        b -= 0.5*db
        return da_1
    
    # 5 训练函数
    def train(self,x,y,max_iter=10000):
        for i in range(max_iter):
            self.forwardPropagation(x)
            #print(self.a3)
            loss = 0.5*np.mean((self.a3-y)**2)
            da = self.a3-y
            da_2 = self.backwardPropagation(da,self.a3,self.a2,self.w3,self.b3,True)
            da_1 = self.backwardPropagation(da_2,self.a2,self.a1,self.w2,self.b2)
            da_0 = self.backwardPropagation(da_1,self.a1,x,self.w1,self.b1)
            self.view_bar(i+1,max_iter,loss)
        return self.a3

    # 6 进程条
    def view_bar(self,step,total,loss):
        rate = step/total
        rate_num = int(rate*40)
        r = '\rstep-%d loss value-%.4f[%s%s]\t%d%% %d/%d'%(step,loss,'>'*rate_num,'-'*(40-rate_num),
                                      int(rate*100),step,total)
        sys.stdout.write(r)
        sys.stdout.flush()
        
if __name__ == '__main__':
    x,y = generate_data()
    plt.scatter(x,y,c='r')
    plt.ion()
    print('plot') 
    dnn = DNN()
    predict = dnn.train(x,y)
    plt.plot(x.flatten(),predict.flatten(),'-')
    plt.show()

27.2 dnn模块

OpenCV自3.3版本开始,加入了对深度学习网络的支持,即DNN模块,它支持主流的深度学习框架生成与到处模型的加载。

27.2.1 模块简介

OpenCV中的深度学习模块(DNN)只提供了推理功能,不涉及模型的训练,支持多种深度学习框架,比如TensorFlow、Caffe、Torch和Darknet。

27.2.2 模块架构

DNN模块的架构如下图所示:

从上往下依次是:

  1. 第一层:语言绑定层,主要支持Python和Java,还包括准确度测试、性能测试和部分示例程序。
  2. 第二层:C++的API层,是原生的API,功能主要包括加载网络模型、推理运算以及获取网络的输出等。
  3. 第三层:实现层,包括模型转换器、DNN引擎以及层实现等。模型转换器将各种网络模型格式转换为DNN模块的内部表示,DNN引擎负责内部网络的组织和优化,层实现指各种层运算的实现过程。
  4. 第四层:加速层,包括CPU加速、GPU加速、Halide加速和Intel推理引擎加速。CPU加速用到了SSE和AVX指令以及大量的多线程元语,而OpenCL加速是针对GPU进行并行运算的加速。Halide是一个实验性的实现,并且性能一般。Intel推理引擎加速需要安装OpenVINO库,它可以实现在CPU、GPU和VPU上的加速,在GPU上内部会调用clDNN库来做GPU上的加速,在CPU上内部会调用MKL-DNN来做CPU加速,而Movidius主要是在VPU上使用的专用库来进行加速。

除了上述的加速方法外,DNN模块还有网络层面的优化。这种优化优化分两类,一类是层融合,还有一类是内存复用。

层融合

层融合通过对网络结构的分析,把多个层合并到一起,从而降低网络复杂度和减少运算量。

如上图所示,卷积层后面的BatchNorm层、Scale层和RelU层都被合并到了卷积层当中。这样一来,四个层运算最终变成了一个层运算。

如上图所示,网络结构将卷积层1和Eltwise Layer和RelU Layer合并成一个卷积层,将卷积层2作为第一个卷积层新增的一个输入。这样一来,原先的四个网络层变成了两个网络层运算。

如上图所示,原始的网络结构把三个层的输出通过连接层连接之后输入到后续层,这种情况可以把中间的连接层直接去掉,将三个网络层输出直接接到第四层的输入上面,这种网络结构多出现SSD类型的网络架构当中。

内存复用

深度神经网络运算过程当中会占用非常大量的内存资源,一部分是用来存储权重值,另一部分是用来存储中间层的运算结果。我们考虑到网络运算是一层一层按顺序进行的,因此后面的层可以复用前面的层分配的内存。

下图是一个没有经过优化的内存重用的运行时的存储结构,红色块代表的是分配出来的内存,绿色块代表的是一个引用内存,蓝色箭头代表的是引用方向。数据流是自下而上流动的,层的计算顺序也是自下而上进行运算。每一层都会分配自己的输出内存,这个输出被后续层引用为输入。

对内存复用也有两种方法:

第一种内存复用的方法是输入内存复用。

如上图所示,如果我们的层运算是一个in-place模式,那么我们无须为输出分配内存,直接把输出结果写到输入的内存当中即可。in-place模式指的是运算结果可以直接写回到输入而不影响其他位置的运算,如每个像素点做一次Scale的运算。类似于in-place模式的情况,就可以使用输入内存复用的方式。

第二种内存复用的方法是后续层复用前面层的输出。

如上图所示,在这个例子中,Layer3在运算时,Layer1和Layer2已经完成了运算。此时,Layer1的输出内存已经空闲下来,因此,Layer3不需要再分配自己的内存,直接引用Layer1的输出内存即可。由于深度神经网络的层数可以非常多,这种复用情景会大量的出现,使用这种复用方式之后,网络运算的内存占用量会下降30%~70%。

27.2.3 常用方法简介

DNN模块有很多可直接调用的Python API接口,现将其介绍如下:

dnn.blobFromImage

作用:根据输入图像,创建维度N(图片的个数),通道数C,高H和宽W次序的blobs

blobs是cv.dnn的输入数据格式,和tensorflow的tensor格式差不多的意思

函数原型:

    blobFromImage(image, 
                  scalefactor=None, 
                  size=None, 
                  mean=None, 
                  swapRB=None, 
                  crop=None, 
                  ddepth=None)

参数:

  1. image:cv2.imread 读取的图片数据
  2. scalefactor: 缩放像素值,如 [0, 255] - [0, 1]
  3. size: 输出blob(图像)的尺寸,如 (netInWidth, netInHeight)
  4. mean: 从各通道减均值. 如果输入 image 为 BGR 次序,且swapRB=True,则通道次序为 (mean-R, mean-G, mean-B).
  5. swapRB: 交换 3 通道图片的第一个和最后一个通道,如 BGR - RGB
  6. crop: 图像尺寸 resize 后是否裁剪. 如果crop=True,则,输入图片的尺寸调整resize后,一个边对应与 size 的一个维度,而另一个边的值大于等于 size 的另一个维度;然后从 resize 后的图片中心进行 crop. 如果crop=False,则无需 crop,只需保持图片的长宽比
  7. ddepth: 输出 blob 的 Depth. 可选: CV_32F 或 CV_8U
dnn.blobFromImages

作用:批量处理图片,创建4维的blob,其它参数类似于 dnn.blobFromImage

函数原型:

   blobFromImages(images, 
                  scalefactor=None, 
                  size=None, 
                  mean=None, 
                  swapRB=None, 
                  crop=None, 
                  ddepth=None)

参数:

  1. images:cv2.imread 读取的图片数据(1、3或者4通道)
  2. scalefactor:图像各通道数值的缩放比例,如 [0, 255] - [0, 1]
  3. size:输出blob(图像)的尺寸,二元组。如size=(200,300)表示高h=300,宽w=200。
  4. mean:用于各通道减去的值,以降低光照的影响。如果输入 image 为 BGR 次序,且swapRB=True,则通道次序为 (mean-R, mean-G, mean-B)。如:image为bgr3通道的图像,mean=[104.0, 177.0, 123.0],表示b通道的值-104,g-177,r-123)
  5. swapRB:默认为False.(cv2.imread读取的是彩图是bgr通道)。交换 3 通道图片的第一个和最后一个通道,如 BGR - RGB
  6. crop: 图像尺寸 resize 后是否裁剪。默认为False。当值为True时,先按比例缩放,然后从中心裁剪成size尺寸。
  7. ddepth: 输出的图像深度,可选CV_32F 或者 CV_8U.
 示例
import cv2
from cv2 import dnn
import numpy as np 
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

img_cv2 = cv2.imread('C:/Users/xxx/Downloads/lena.jpg') # 读取图片
print("原图像大小: ", img_cv2.shape)# 原图像大小:  (740, 960, 3)

inWidth = 256
inHeight = 256
outBlob1 = cv2.dnn.blobFromImage(img_cv2,
                                scalefactor=1.0 / 255,  # 归一化,
                                size=(inWidth, inHeight),
                                mean=(0, 0, 0),
                                swapRB=False,  # 不转换为RGB
                                crop=False)
print("未裁剪输出: ", outBlob1.shape)  # 输出为:(图像数,通道数,高,宽)  未裁剪输出:  (1, 3, 256, 256)
outimg1 = np.transpose(outBlob1[0], (1, 2, 0))  # outBlob1[0]是第一副图像数据,后面是把通道数,高,宽做一个转换位置--》高,宽,通道数

outBlob2 = cv2.dnn.blobFromImage(img_cv2,
                                scalefactor=1.0 / 255,
                                size=(inWidth, inHeight),
                                mean=(0, 0, 0),
                                swapRB=False,
                                crop=True)  # 进行裁剪,不缩放
print("裁剪输出: ", outBlob2.shape)  # 裁剪输出:  (1, 3, 256, 256)
outimg2 = np.transpose(outBlob2[0], (1, 2, 0))

plt.figure(figsize=[14.4, 4.8])
plt.subplot(1, 3, 1)
plt.title('输入图像', fontsize=16)
plt.imshow(cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.subplot(1, 3, 2)
plt.title('输出图像 - 未裁剪', fontsize=16)
plt.imshow(cv2.cvtColor(outimg1, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.subplot(1, 3, 3)
plt.title('输出图像 - 裁剪', fontsize=16)
plt.imshow(cv2.cvtColor(outimg2, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.show()

dnn.NMSBoxes
  1. NMS介绍:检测器预测完结果,一张图像中会有很多预测框,预测的结果间可能存在高冗余(即同一个目标可能被预测多个矩形框),进行NMS是在同一Ground Truth的多个预测结果时,我们只取置信度分数最高的那个预测结果,其余的我们都不要

缺点:NMS时,置信度分高但是位置不够准的框可能会把置信度分低但是位置很准的框去掉。

NMS过程

  1. NMS的思路是:对于一张图片中的每一个预测框来说,模型为其每一个类别都预测了一个置信度分数(一般多分类,模型输出后接softmax,每一个类别都得到了一个置信度分数,包括背景类)我们取置信度最高的那一个类别作为预测框中对象所属的类别。1. 首先我们将置信度分数低于置信度阈值a的所有预测框去掉 。2. 然后在同一张图片上,我们按照类别(除开背景类,因为背景类不需要进行NMS),将所有预测框按照置信度从高到低排序,将置信度最高的框作为我们要保留的此类别的第1个预测框,3. 然后按照顺序计算剩下其他预测框与其的IoU,4. 去掉与其IoU(IoU作为置信度分数)大于IoU阈值b的预测框(其实代码实现里是将这些要去掉的预测框其置信度分数置为0),5. 第一次迭代结束,我们已经剔除了与第一个框重合度较高的框。
  2. 接着从剩下的预测框中取置信度分数最高的检测框作为我们要保留的第2个预测框,进行第2次迭代。反复下去,我们就过滤掉此类别与同一GT重叠度较高的预测框了,然后对下一个类别处理,直至处理完所有的类别。
  3. IoU 的全称为交并比(Intersection over Union),通过这个名称我们大概可以猜到 IoU 的计算方法。IoU 计算的是 “预测的边框” 和 “真实的边框” 的交集和并集的比值。

作用:根据给定的检测boxes和对应的scores进行NMS(非极大值抑制)处理

函数原型:

     MSBoxes(bboxes, 
             scores, 
             score_threshold, 
             nms_threshold, 
             eta=None, 
             top_k=None)

参数:

  1. boxes: 待处理的边界框 bounding boxes
  2. scores: 对于于待处理边界框的 scores
  3. score_threshold: 用于过滤 boxes 的 score 阈值
  4. nms_threshold: NMS 用到的阈值
  5. indices: NMS 处理后所保留的边界框的索引值
  6. eta: 自适应阈值公式中的相关系数:
  7. top_k: 如果 top_k>0,则保留最多 top_k 个边界框索引值.
dnn.readNet

作用:加载深度学习网络及其模型参数

函数原型:

readNet(model, config=None, framework=None)

参数:

  1. model: 训练的权重参数的模型二值文件,支持的格式有:*.caffemodel(Caffe)、*.pb(TensorFlow)、*.t7 或 *.net(Torch)、 *.weights(Darknet)、*.bin(DLDT).
  2. config: 包含网络配置的文本文件,支持的格式有:*.prototxt (Caffe)、*.pbtxt (TensorFlow)、*.cfg (Darknet)、*.xml (DLDT).
  3. framework: 所支持格式的框架名

该函数自动检测训练模型所采用的深度框架,然后调用 readNetFromCaffereadNetFromTensorflowreadNetFromTorchreadNetFromDarknet 中的某个函数完成深度学习网络模型及模型参数的加载。

下面我们看下对应于特定框架的API:

Caffe

作用:加载采用Caffe的配置网络和训练的权重参数

API:readNetFromCaffe(prototxt, caffeModel=None)
Darknet

作用:加载采用Darknet的配置网络和训练的权重参数

API:readNetFromDarknet(cfgFile, DarknetModel=None)
Tensorflow

作用:加载采用Tensorflow的配置网络和训练的权重参数

API:readNetFromTensorflow(model, config=None)

参数:

  1. model: .pb 文件
  2. config: .pbtxt 文件
Torch

作用:加载采用Torch的配置网络和训练的权重参数

API:readNetFromTorch(model, isBinary=None)

参数:

  1. model: 采用 torch.save()函数保存的文件
  2. isBinary:
ONNX

作用:加载.ONNX模型网络和权重参数

API:readNetFromONNX(onnxFile)

参数:

  1. onnxFile:

27.3 展望

OpenCV的DNN模块,支持主流各种神经网络架构,各位看官可以选择其中一种。在OpenCV专辑不再详述。

至此,有关Opencv的基本介绍就算告一段落了。还有相机标定、随机采样一致算法(RANSAC)算法、使用二维特征点(Features2D)和单映射(Homography)寻找已知物体等桩体没有介绍。各位若有兴趣,可自行找相关资料学习。

若有时间,我会写几个实战案例。如银行卡号识别、OCR识别、图像拼接、停车场车位识别、目标追踪、卷积操作、人脸识别等等。

有关《机器学习》的部分,我会再写一系列的博文,介绍机器学习背后的基本理论,也欢迎各位赏光。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐