NNDL 实验六 卷积神经网络(1) 卷积

目录

5.1 卷积

5.1.1 二维卷积运算

5.1.2 二维卷积算子

5.1.3 二维卷积的参数量和计算量

5.1.4 感受野

5.1.5 卷积的变种

5.1.5.1 步长(Stride)

 5.1.5.2 零填充(Zero Padding)

5.1.6 带步长和零填充的二维卷积算子

5.1.7 使用卷积运算完成图像边缘检测任务

 选做题:

边缘检测系列1:传统边缘检测算子

边缘检测系列2:简易的 Canny 边缘检测器

边缘检测系列3:【HED】 Holistically-Nested 边缘检测

边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测

 边缘检测系列5:【CED】添加了反向细化路径的 HED 模型

 心得体会

参考文献


5.1 卷积

考虑到使用全连接前馈网络来处理图像时,会出现如下问题:

  1. 模型参数过多,容易发生过拟合。 在全连接前馈网络中,隐藏层的每个神经元都要跟该层所有输入的神经元相连接。随着隐藏层神经元数量的增多,参数的规模也会急剧增加,导致整个神经网络的训练效率非常低,也很容易发生过拟合。

  2. 难以提取图像中的局部不变性特征。 自然图像中的物体都具有局部不变性特征,比如尺度缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变性特征。

卷积神经网络有三个结构上的特性:局部连接、权重共享和汇聚。这些特性使得卷积神经网络具有一定程度上的平移、缩放和旋转不变性。和前馈神经网络相比,卷积神经网络的参数也更少。因此,通常会使用卷积神经网络来处理图像信息。

5.1.1 二维卷积运算

在机器学习和图像处理领域,卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。在计算卷积的过程中,需要进行卷积核的翻转,而这也会带来一些不必要的操作和开销。因此,在具体实现上,一般会以数学中的互相关(Cross-Correlatio)运算来代替卷积。
在神经网络中,卷积运算的主要作用是抽取特征,卷积核是否进行翻转并不会影响其特征抽取的能力。特别是当卷积核是可学习的参数时,卷积和互相关在能力上是等价的。因此,很多时候,为方便起见,会直接用互相关来代替卷积。

NNDL 实验六 卷积神经网络(1) 卷积_第1张图片

NNDL 实验六 卷积神经网络(1) 卷积_第2张图片

 经过卷积运算后,最终输出矩阵大小则为

M^{'}=M-U+1

N^{'}=N-U+1

可以发现,使用卷积处理图像,会有以下两个特性:

  1. 在卷积层(假设是第l层)中的每一个神经元都只和前一层(第l−1层)中某个局部窗口内的神经元相连,构成一个局部连接网络,这也就是卷积神经网络的局部连接特性。
  2. 由于卷积的主要功能是在一个图像(或特征图)上滑动一个卷积核,所以作为参数的卷积核W∈RU×V对于第l层的所有的神经元都是相同的,这也就是卷积神经网络的权重共享特性。

5.1.2 二维卷积算子

 在本书后面的实现中,算子都继承paddle.nn.Layer,并使用支持反向传播的飞桨API进行实现,这样我们就可以不用手工写backword()的代码实现。

使用pytorch实现

import torch
import torch.nn as nn
 
class Conv2D(nn.Module):
    def __init__(self, kernel_size,
                    weight_attr=torch.tensor([[0., 1.],[2., 3.]])):
        super(Conv2D, self).__init__()
        # 使用'paddle.create_parameter'创建卷积核
        # 使用'paddle.ParamAttr'进行参数初始化
        self.weight = nn.Parameter(weight_attr)
    def forward(self, X):
        """
        输入:
            - X:输入矩阵,shape=[B, M, N],B为样本数量
        输出:
            - output:输出矩阵
        """
        u, v = self.weight.shape
        output = torch.zeros([X.shape[0], X.shape[1] - u + 1, X.shape[2] - v + 1])
        for i in range(output.shape[1]):
            for j in range(output.shape[2]):
                output[:, i, j] = torch.sum(X[:, i:i+u, j:j+v]*self.weight, axis=[1,2])
        return output
 
# 随机构造一个二维输入矩阵
torch.seed()
inputs = torch.tensor([[[1.,2.,3.],[4.,5.,6.],[7.,8.,9.]]])
 
conv2d = Conv2D(kernel_size=2)
outputs = conv2d(inputs)
print("input: {}, \noutput: {}".format(inputs, outputs))
 
torch.Size([2, 2])
input: tensor([[[1., 2., 3.],
         [4., 5., 6.],
         [7., 8., 9.]]]), 
output: tensor([[[25., 31.],
         [43., 49.]]], grad_fn=)

5.1.3 二维卷积的参数量和计算量

由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,假设有一幅大小为32×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达1025个,此时该层的计算过程如 图5.3 所示。

NNDL 实验六 卷积神经网络(1) 卷积_第3张图片

 可以想像,随着隐藏层神经元数量的变多以及层数的加深,使用全连接前馈网络处理图像数据时,参数量会急剧增加。

如果使用卷积进行图像处理,当卷积核为3\times 3时,参数量仅为9,相较于全连接前馈网络,参数量少了非常多。

计算量

在卷积神经网络中运算时,通常会统计网络总的乘加运算次数作为计算量(FLOPs,floating point of operations),来衡量整个网络的运算速度。对于单个二维卷积,计算量的统计方式为:

FLOPs=M^{'}\times N^{'}\times U\times V

其中M′×N′表示输出特征图的尺寸,即输出特征图上每个点都要与卷积核W∈RU×V进行U×V次乘加运算。对于一幅大小为32×32的图像,使用3×3的卷积核进行运算可以得到以下的输出特征图尺寸:

M^{'}=M-U+1=30

N^{'}=N-V+1=30

此时计算量为:

FLOPs=M^{'}\times N^{'}\times U\times V=30\times 30\times 3\times 3=8100

5.1.4 感受野

输出特征图上每个点的数值,是由输入图片上大小为U×V的区域的元素与卷积核每个元素相乘再相加得到的,所以输入图像上U×V区域内每个元素数值的改变,都会影响输出点的像素值。我们将这个区域叫做输出特征图上对应点的感受野。感受野内每个元素数值的变动,都会影响输出点的数值变化。比如3×3卷积对应的感受野大小就是3×3,如图所示。

NNDL 实验六 卷积神经网络(1) 卷积_第4张图片

 而当通过两层3×3的卷积之后,感受野的大小将会增加到5×5,如图所示。

NNDL 实验六 卷积神经网络(1) 卷积_第5张图片

因此,当增加卷积网络深度的同时,感受野将会增大,输出特征图中的一个像素点将会包含更多的图像语义信息。

5.1.5 卷积的变种

在卷积的标准定义基础上,还可以引入卷积核的滑动步长和零填充来增加卷积的多样性,从而更灵活地进行特征抽取。

5.1.5.1 步长(Stride)

 在卷积运算的过程中,有时会希望跳过一些位置来降低计算的开销,也可以把这一过程看作是对标准卷积运算输出的下采样

在计算卷积时,可以在所有维度上每间隔S个元素计算一次,S称为卷积运算的步长(Stride),也就是卷积核在滑动时的间隔。

NNDL 实验六 卷积神经网络(1) 卷积_第6张图片

 NNDL 实验六 卷积神经网络(1) 卷积_第7张图片

 5.1.5.2 零填充(Zero Padding)

在卷积运算中,还可以对输入用零进行填充使得其尺寸变大。根据卷积的定义,如果不进行填充,当卷积核尺寸大于1时,输出特征会缩减。对输入进行零填充则可以对卷积核的宽度和输出的大小进行独立的控制。

在二维卷积运算中,零填充(Zero Padding)是指在输入矩阵周围对称地补上P个0。图5.7 为使用零填充的示例。

NNDL 实验六 卷积神经网络(1) 卷积_第8张图片

NNDL 实验六 卷积神经网络(1) 卷积_第9张图片 

 通常情况下,在层数较深的卷积神经网络,比如:VGG、ResNet中,会使用等宽卷积保证输出特征图的大小不会随着层数的变深而快速缩减。例如:当卷积核的大小为3×3时,会将步长设置为S=1,两端补零P=1,此时,卷积后的输出尺寸就可以保持不变。在本章后续的案例中,会使用ResNet进行实验。

5.1.6 带步长和零填充的二维卷积算子

从输出结果看出,使用3×3大小卷积,

padding为1,

stride=1时,模型的输出特征图与输入特征图保持一致;

stride=2时,模型的输出特征图的宽和高都缩小一倍。

class Conv2D(nn.Module):
    def __init__(self, kernel_size,stride=1, padding=0,
                    weight_attr=torch.tensor([[0., 1.],[2., 3.]])):
        super(Conv2D, self).__init__()
        # 使用'paddle.create_parameter'创建卷积核
        # 使用'paddle.ParamAttr'进行参数初始化
        self.weight = nn.Parameter(weight_attr)
        # 步长
        self.stride = stride
        # 零填充
        self.padding = padding
 
    def forward(self, X):
        """
        输入:
            - X:输入矩阵,shape=[B, M, N],B为样本数量
        输出:
            - output:输出矩阵
        """
        u, v = self.weight.shape
        output = torch.zeros([X.shape[0], X.shape[1] - u + 1, X.shape[2] - v + 1])
        for i in range(output.shape[1]):
            for j in range(output.shape[2]):
                output[:, i, j] = torch.sum(X[:, i:i+u, j:j+v]*self.weight, axis=[1,2])
        return output
 
# 随机构造一个二维输入矩阵
torch.seed()
inputs = torch.tensor([[[1.,2.,3.],[4.,5.,6.],[7.,8.,9.]]])
 
conv2d = Conv2D(kernel_size=2)
outputs = conv2d(inputs)
print("input: {}, \noutput: {}".format(inputs, outputs))
 
inputs = torch.randn(size=[2, 8, 8])
conv2d_padding = Conv2D(kernel_size=3, padding=1)
outputs = conv2d_padding(inputs)
print("When kernel_size=3, padding=1 stride=1, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))
conv2d_stride = Conv2D(kernel_size=3, stride=2, padding=1)
outputs = conv2d_stride(inputs)
print("When kernel_size=3, padding=1 stride=2, input's shape: {}, output's shape: {}".format(inputs.shape, outputs.shape))
When kernel_size=3, padding=1 stride=1, input's shape: torch.Size([2, 8, 8]), output's shape: torch.Size([2, 8, 8])
When kernel_size=3, padding=1 stride=2, input's shape: torch.Size([2, 8, 8]), output's shape: torch.Size([2, 4, 4])

5.1.7 使用卷积运算完成图像边缘检测任务

NNDL 实验六 卷积神经网络(1) 卷积_第10张图片

 

import torch
import torch.nn as nn
import torch.nn
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

class Conv2D(nn.Module):
    def __init__(self, kernel_size,stride=1, padding=0):
        super(Conv2D, self).__init__()
        # 设置卷积核参数
        w = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype='float32').reshape((3,3))
        w=torch.from_numpy(w)
        self.weight = torch.nn.Parameter(w, requires_grad=True)
        self.stride = stride
        self.padding = padding

    def forward(self, X):
        # 零填充
        new_X = torch.zeros([X.shape[0], X.shape[1] + 2 * self.padding, X.shape[2] + 2 * self.padding])
        new_X[:, self.padding:X.shape[1] + self.padding, self.padding:X.shape[2] + self.padding] = X
        u, v = self.weight.shape
        output_w = (new_X.shape[1] - u) // self.stride + 1
        output_h = (new_X.shape[2] - v) // self.stride + 1
        output = torch.zeros([X.shape[0], output_w, output_h])
        for i in range(0, output.shape[1]):
            for j in range(0, output.shape[2]):
                output[:, i, j] = torch.sum(
                    new_X[:, self.stride * i:self.stride * i + u, self.stride * j:self.stride * j + v] * self.weight,
                    axis=[1, 2])
        return output


# 读取图片
img = Image.open(r'jju.jpg').convert('L')
img = np.array(img, dtype='float32')
im = torch.from_numpy(img.reshape((img.shape[0],img.shape[1])))

# 创建卷积算子,卷积核大小为3x3,并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D(kernel_size=3, stride=1, padding=0)

# 将读入的图片转化为float32类型的numpy.ndarray
inputs = np.array(im).astype('float32')
print("bf as_tensor, inputs:",inputs)
# 将图片转为Tensor
inputs = torch.as_tensor(inputs)
print("bf unsqueeze, inputs:",inputs)
inputs = torch.unsqueeze(inputs, axis=0)
print("af unsqueeze, inputs:",inputs)
outputs = conv(inputs)
print(outputs)
# outputs = outputs.data.squeeze().numpy()
# # 可视化结果
plt.subplot(121).set_title('input image', fontsize=15)
plt.imshow(img.astype('uint8'),cmap='gray')
plt.subplot(122).set_title('output feature map', fontsize=15)
plt.imshow(outputs.squeeze().detach().numpy(), cmap='gray')
plt.savefig('conv-vis.pdf')
plt.show()

NNDL 实验六 卷积神经网络(1) 卷积_第11张图片

 选做题:

边缘检测系列1:传统边缘检测算子

边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio

实现一些传统边缘检测算子,如:Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian

def test_edge_det(kernel, img_path='./.number.png'):
    img = cv2.imread(img_path, 0)
    img_tensor = torch.tensor(img, dtype=torch.float32)[None, None, ...]
    op = EdgeOP(kernel)
    all_results = []
    for mode in range(4):
        results = op(img_tensor, mode=mode)
        all_results.append(results.numpy()[0])
 
    results = op(img_tensor, mode=4)
    for result in results.numpy()[0]:
        all_results.append(result)
    return all_results, np.concatenate(all_results, 1)
  • 因为上述的这些算子在本质上都是通过卷积计算实现的,只是所使用到的卷积核参数有所不同
  • 所以可以构建一个通用的计算算子,只需要传入对应的卷积核参数即可实现不同的边缘检测
  • 并且在后处理时集成了上述的四种计算最终边缘强度的方式

Roberts算子

NNDL 实验六 卷积神经网络(1) 卷积_第12张图片

roberts_kernel = np.array([
    [[
        [1,  0],
        [0, -1]
    ]],
    [[
        [0, -1],
        [1,  0]
    ]]
])

_, concat_res = test_edge_det(roberts_kernel)
Image.fromarray(concat_res)

Prewitt

NNDL 实验六 卷积神经网络(1) 卷积_第13张图片

 

prewitt_kernel = np.array([
    [[
        [-1, -1, -1],
        [ 0,  0,  0],
        [ 1,  1,  1]
    ]],
    [[
        [-1,  0,  1],
        [-1,  0,  1],
        [-1,  0,  1]
    ]],
    [[
        [ 0,  1,  1],
        [-1,  0,  1],
        [-1, -1,  0]
    ]],
    [[
        [ -1, -1,  0],
        [ -1,  0,  1],
        [  0,  1,  1]
    ]]
])

_, concat_res = test_edge_det(prewitt_kernel)
Image.fromarray(concat_res)

Sobel

NNDL 实验六 卷积神经网络(1) 卷积_第14张图片

 

sobel_kernel = np.array([
    [[
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ]],
    [[
        [-1,  0,  1],
        [-2,  0,  2],
        [-1,  0,  1]
    ]],
    [[
        [ 0,  1,  2],
        [-1,  0,  1],
        [-2, -1,  0]
    ]],
    [[
        [ -2, -1,  0],
        [ -1,  0,  1],
        [  0,  1,  2]
    ]]
])

_, concat_res = test_edge_det(sobel_kernel)
Image.fromarray(concat_res)

Scharr

NNDL 实验六 卷积神经网络(1) 卷积_第15张图片

 

scharr_kernel = np.array([
    [[
        [-3, -10, -3],
        [ 0,   0,  0],
        [ 3,  10,  3]
    ]],
    [[
        [-3,  0,   3],
        [-10, 0,  10],
        [-3,  0,   3]
    ]],
    [[
        [ 0,  3,  10],
        [-3,  0,  3],
        [-10, -3,  0]
    ]],
    [[
        [ -10, -3, 0],
        [ -3,  0, 3],
        [ 0,  3,  10]
    ]]
])

_, concat_res = test_edge_det(scharr_kernel)
Image.fromarray(concat_res)

Kirsch

NNDL 实验六 卷积神经网络(1) 卷积_第16张图片

 

Krisch_kernel = np.array([
    [[
        [5, 5, 5],
        [-3,0,-3],
        [-3,-3,-3]
    ]],
    [[
        [-3, 5,5],
        [-3,0,5],
        [-3,-3,-3]
    ]],
    [[
        [-3,-3,5],
        [-3,0,5],
        [-3,-3,5]
    ]],
    [[
        [-3,-3,-3],
        [-3,0,5],
        [-3,5,5]
    ]],
    [[
        [-3, -3, -3],
        [-3,0,-3],
        [5,5,5]
    ]],
    [[
        [-3, -3, -3],
        [5,0,-3],
        [5,5,-3]
    ]],
    [[
        [5, -3, -3],
        [5,0,-3],
        [5,-3,-3]
    ]],
    [[
        [5, 5, -3],
        [5,0,-3],
        [-3,-3,-3]
    ]],
])

_, concat_res = test_edge_det(Krisch_kernel)
Image.fromarray(concat_res)

Robinson

NNDL 实验六 卷积神经网络(1) 卷积_第17张图片

 

robinson_kernel = np.array([
    [[
        [1, 2, 1],
        [0, 0, 0],
        [-1, -2, -1]
    ]],
    [[
        [0, 1, 2],
        [-1, 0, 1],
        [-2, -1, 0]
    ]],
    [[
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ]],
    [[
        [-2, -1, 0],
        [-1, 0, 1],
        [0, 1, 2]
    ]],
    [[
        [-1, -2, -1],
        [0, 0, 0],
        [1, 2, 1]
    ]],
    [[
        [0, -1, -2],
        [1, 0, -1],
        [2, 1, 0]
    ]],
    [[
        [1, 0, -1],
        [2, 0, -2],
        [1, 0, -1]
    ]],
    [[
        [2, 1, 0],
        [1, 0, -1],
        [0, -1, -2]
    ]],
])

_, concat_res = test_edge_det(robinson_kernel)
Image.fromarray(concat_res)

Laplacian

NNDL 实验六 卷积神经网络(1) 卷积_第18张图片

 

laplacian_kernel = np.array([
    [[
        [1, 1, 1],
        [1, -8, 1],
        [1, 1, 1]
    ]],
    [[
        [0, 1, 0],
        [1, -4, 1],
        [0, 1, 0]
    ]]
])

_, concat_res = test_edge_det(laplacian_kernel)
Image.fromarray(concat_res)

边缘检测系列2:简易的 Canny 边缘检测器

简易的 Canny 边缘检测器 - 飞桨AI Studio

 

  • Canny 是一个经典的图像边缘检测算法,一般包含如下几个步骤:

    • 使用高斯模糊对图像进行模糊降噪处理

    • 基于图像梯度幅值进行图像边缘增强

    • 非极大值抑制处理进行图像边缘细化

    • 图像二值化和边缘连接得到最终的结果

import cv2
# 加载图像
image = cv2.imread(r'jju.jpg',0)
image = cv2.resize(image,(800,800))
def Canny(image,k,t1,t2):
    img = cv2.GaussianBlur(image, (k, k), 0)
    canny = cv2.Canny(img, t1, t2)
    return canny
image = cv2.resize(image, (800, 600))
cv2.imshow('Original Image', image)
output =cv2.resize(Canny(image,3,50,150),(800,600))
cv2.imshow('Canny Image', output)
# 停顿
if cv2.waitKey(0) & 0xFF == 27:
    cv2.destroyAllWindows()

边缘检测系列3:【HED】 Holistically-Nested 边缘检测

边缘检测系列3:【HED】 Holistically-Nested 边缘检测 - 飞桨AI Studio

复现论文 Holistically-Nested Edge Detection,发表于 CVPR 2015 

一个基于深度学习的端到端边缘检测模型

08e50a26da564a72aa021cf450784d55.jpeg

 a4facf95480645b58a6d2148366ae764.jpeg

 

  • HED 模型包含五个层级的特征提取架构,每个层级中:

    • 使用 VGG Block 提取层级特征图

    • 使用层级特征图计算层级输出

    • 层级输出上采样

  • 最后融合五个层级输出作为模型的最终输出:

    • 通道维度拼接五个层级的输出

    • 1x1 卷积对层级输出进行融合

边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测

 边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测 - 飞桨AI Studio (baidu.com)

 5bad44d0f55645d3bf3a4918da303075.jpeg

 

  • HED 模型包含五个层级的特征提取架构,每个层级中:

    • 使用 VGG Block 提取层级特征图

    • 使用层级特征图计算层级输出

    • 层级输出上采样

  • 最后融合五个层级输出作为模型的最终输出:

    • 通道维度拼接五个层级的输出

    • 1x1 卷积对层级输出进行融合

 76fd7ab6979e4bdea26c475806e40bc5.jpeg

 

 边缘检测系列5:【CED】添加了反向细化路径的 HED 模型

 边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 - 飞桨AI Studio (baidu.com)

Crisp Edge Detection(CED)模型是前面介绍过的 HED 模型的另一种改进模型

 1f2b9e9a6eca4db7bcc05d242b739ed8.jpeg

CED 模型总体基于 HED 模型改造而来,其中做了如下几个改进:

  • 将模型中的上采样操作从转置卷积插值更换为 PixelShuffle
  • 添加了反向细化路径,即一个反向的从高层级特征逐步往低层级特征的边缘细化路径
  • 没有多层级输出,最终的输出为融合了各层级的特征的边缘检测结果

架构图如下:

 

 5ecd3e1f5cb54cecb1e4ca6f815cda22.jpeg

 心得体会

本次实验是对一些专业名词的代码实现,可以说是对上次作业的补充,上次作业也只是浅浅的谈了一下定义,也不能算是第二次学了吧,本次学习还是有很大启发的,算是对机器学习中漏洞的填补吧,本次选做体中实现了几种常用的传统边缘检测算子,像Roberts、Prewitt、Sobel、Scharr、Kirsch、Robinson、Laplacian,实现了简单的Canny边缘检测器,通过阅读文中连接对边缘检测内容有了更深的印象。。。

参考文献

NNDL 实验六 卷积神经网络(1)卷积

边缘检测系列1:传统边缘检测算子 - 飞桨AI Studio
边缘检测系列2:简易的 Canny 边缘检测器 - 飞桨AI Studio
 

 边缘检测系列3:【HED】 Holistically-Nested 边缘检测 - 飞桨AI Studio

 边缘检测系列4:【RCF】基于更丰富的卷积特征的边缘检测 - 飞桨AI Studio (baidu.com)

 边缘检测系列5:【CED】添加了反向细化路径的 HED 模型 - 飞桨AI Studio (baidu.com)

 NNDL 实验5(上) - HBU_DAVID - 博客园 (cnblogs.com)

你可能感兴趣的:(cnn,深度学习,计算机视觉)