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 边缘检测器

总结


5.1 卷积

卷积神经网络(Convolutional Neural Network,CNN)

  • 受生物学上感受野机制的启发而提出。
  • 一般是由卷积层、汇聚层和全连接层交叉堆叠而成的前馈神经网络
  • 有三个结构上的特性:局部连接、权重共享、汇聚。
  • 具有一定程度上的平移、缩放和旋转不变性。
  • 和前馈神经网络相比,卷积神经网络的参数更少。
  • 主要应用在图像和视频分析的任务上,其准确率一般也远远超出了其他的神经网络模型。
  • 近年来卷积神经网络也广泛地应用到自然语言处理、推荐系统等领域。

5.1.1 二维卷积运算

图5.2:卷积操作的计算过程

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

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

5.1.2 二维卷积算子

根据公式(5.1),我们首先实现一个简单的二维卷积算子,代码实现如下:

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__()
        self.weight = torch.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, [1, 2])
        return output


# 随机构造一个二维输入矩阵
torch.manual_seed(100)
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))

结果如下:

input: tensor([[[1., 2., 3.],
         [4., 5., 6.],
         [7., 8., 9.]]]), 
output: tensor([[[25., 31.],
         [43., 49.]]], grad_fn=)

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

参数量

由于二维卷积的运算方式为在一个图像(或特征图)上滑动一个卷积核,通过卷积操作得到一组新的特征。所以参数量仅仅与卷积核的尺寸有关,对于一个输入矩阵X∈RM×NX∈RM×N和一个滤波器W\in R^{U*V},卷积核的参数量为U*V

假设有一幅大小为32×32的图像,如果使用全连接前馈网络进行处理,即便第一个隐藏层神经元个数为1,此时该层的参数量也高达1025个,此时该层的计算过程如 图5.3 所示。

NNDL 实验六 卷积神经网络(1)卷积_第1张图片 图5.3:使用全连接前馈网络处理图像数据的计算过程

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

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

计算量

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

FLOPs={M}'*{N}'*U*V

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

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

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

此时,计算量为:

FLOPs={M}'*{N}'*U*V=30*30*3*3=8100

5.1.4 感受野

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

NNDL 实验六 卷积神经网络(1)卷积_第2张图片 图5.4:感受野为3×3的卷积

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

NNDL 实验六 卷积神经网络(1)卷积_第3张图片 图5.5:感受野为5×5的卷积

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

5.1.5 卷积的变种

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

5.1.5.1 步长(Stride)

在二维卷积运算中,当步长S=2时,计算过程如 图5.6 所示。

NNDL 实验六 卷积神经网络(1)卷积_第4张图片 图5.6:步长为2的二维卷积计算过程

5.1.5.2 零填充(Zero Padding)

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

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

NNDL 实验六 卷积神经网络(1)卷积_第5张图片 图5.7:padding=1的零填充

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

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

引入步长和零填充后,二维卷积算子代码实现如下:

class Conv2D2(nn.Module):
    def __init__(self, kernel_size, stride, padding, weight_attr):
        super(Conv2D2, self).__init__()
        self.weight = nn.Parameter(weight_attr)
        # 步长
        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,
                    [1, 2])
        return output


inputs = torch.randn(size=[2, 8, 8])
w = torch.ones([3, 3])
conv2d_padding = Conv2D2(kernel_size=3, padding=1, stride=1, weight_attr=w)
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 = Conv2D2(kernel_size=3, stride=2, padding=1, weight_attr=w)
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])

从输出结果看出,使用3×3大小卷积,padding为1,当stride=1时,模型的输出特征图可以与输入特征图保持一致;当stride=2时,输出特征图的宽和高都缩小一倍。

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

在图像处理任务中,常用拉普拉斯算子对物体边缘进行提取,拉普拉斯算子为一个大小为3×3的卷积核,中心元素值是88,其余元素值是−1。

下面我们利用上面定义的Conv2D算子,构造一个简单的拉普拉斯算子,并对一张输入的灰度图片进行边缘检测,提取出目标的外形轮廓。

import matplotlib.pyplot as plt
from PIL import Image
import numpy as np
from skimage.color import rgb2gray

# 读取图片
img0 = Image.open('number.jpg').resize((256, 256))
img = rgb2gray(img0)
# 设置卷积核参数
w = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]],dtype='float32')
# 创建卷积算子,卷积核大小为3x3,并使用上面的设置好的数值作为卷积核权重的初始化参数
conv = Conv2D2(kernel_size=3, stride=1, padding=0, weight_attr=torch.tensor(w))

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

 结果如下:

bf to_tensor, inputs: [[0.         0.         0.         ... 0.         0.04313726 0.2627451 ]
 [0.         0.         0.         ... 0.         0.07450981 0.32941177]
 [0.         0.         0.         ... 0.         0.07450981 0.32941177]
 ...
 [0.         0.         0.         ... 0.         0.07450981 0.32941177]
 [0.         0.         0.         ... 0.         0.07450981 0.32941177]
 [0.         0.         0.         ... 0.         0.07450981 0.32941177]]
bf unsqueeze, inputs: tensor([[0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0431, 0.2627],
        [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
        [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
        ...,
        [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
        [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
        [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294]])
af unsqueeze, inputs: tensor([[[0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0431, 0.2627],
         [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
         [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
         ...,
         [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
         [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294],
         [0.0000, 0.0000, 0.0000,  ..., 0.0000, 0.0745, 0.3294]]])

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

从输出结果看,使用拉普拉斯算子,目标的边缘可以成功被检测出来。 

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

引入

  • 图像的边缘指的是灰度值发生急剧变化的位置。

  • 在图像形成过程中,由于亮度、纹理、颜色、阴影等物理因素的不同而导致图像灰度值发生突变,从而形成边缘。

  • 边缘是通过检查每个像素的邻域并对其灰度变化进行量化的,这种灰度变化的量化相当于微积分里连续函数中方向导数或者离散数列的差分。

算法原理

  • 传统的边缘检测大多数是通过基于方向导数掩码(梯度方向导数)求卷积的方法。

  • 计算灰度变化的卷积算子包含Roberts算子、Prewitt算子、Sobel算子、Scharr算子、Kirsch算子、Robinson算子、Laplacian算子。

  • 大多数边缘检测算子是基于方向差分卷积核求卷积的方法,在使用由两个或者多个卷积核组成的边缘检测算子时假设有 n 个卷积核,记 Conv1,Conv2,...,ConvnConv_1, Conv_2, ..., Conv_nConv1​,Conv2​,...,Convn​,为图像分别与个卷积核做卷积的结果,通常有四种方式来衡量最后输出的边缘强度。

  1. 取对应位置绝对值的和:\sum_{i=1}^{n}\left | conv_{i} \right |

  2. 取对应位置平方和的开方:\sqrt{\sum_{i=1}^{n}\left | conv_{i} \right |}

  3. 取对应位置绝对值的最大值:max{∣conv1​∣,∣conv2​∣,...,∣convi​∣}

  4. 插值法:\sum_{i=1}^{n}\left | conv_{i} \right |,其中 ai>=0a_i >= 0ai​>=0

代码实现

import cv2
import numpy as np

# 加载图像
image = cv2.imread('number.jpg', 0)
image = cv2.resize(image, (800, 800))
# 自定义卷积核
# Roberts边缘算子
kernel_Roberts_x = np.array([
    [1, 0],
    [0, -1]
])
kernel_Roberts_y = np.array([
    [0, -1],
    [1, 0]
])
# Sobel边缘算子
kernel_Sobel_x = np.array([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1]])
kernel_Sobel_y = np.array([
    [1, 2, 1],
    [0, 0, 0],
    [-1, -2, -1]])
# Prewitt边缘算子
kernel_Prewitt_x = np.array([
    [-1, 0, 1],
    [-1, 0, 1],
    [-1, 0, 1]])
kernel_Prewitt_y = np.array([
    [1, 1, 1],
    [0, 0, 0],
    [-1, -1, -1]])


# Kirsch 边缘检测算子
def kirsch(image):
    m, n = image.shape
    list = []
    kirsch = np.zeros((m, n))
    for i in range(2, m - 1):
        for j in range(2, n - 1):
            d1 = np.square(5 * image[i - 1, j - 1] + 5 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] - 3 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d2 = np.square((-3) * image[i - 1, j - 1] + 5 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d3 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] + 5 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d4 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] -
                           3 * image[i, j - 1] + 5 * image[i, j + 1] - 3 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d5 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] - 3
                           * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] + 5 * image[i + 1, j + 1])
            d6 = np.square((-3) * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] +
                           5 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d7 = np.square(5 * image[i - 1, j - 1] - 3 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] + 5 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])
            d8 = np.square(5 * image[i - 1, j - 1] + 5 * image[i - 1, j] - 3 * image[i - 1, j + 1] +
                           5 * image[i, j - 1] - 3 * image[i, j + 1] - 3 * image[i + 1, j - 1] -
                           3 * image[i + 1, j] - 3 * image[i + 1, j + 1])

            # 第一种方法:取各个方向的最大值,效果并不好,采用另一种方法
            list = [d1, d2, d3, d4, d5, d6, d7, d8]
            kirsch[i, j] = int(np.sqrt(max(list)))
    for i in range(m):
        for j in range(n):
            if kirsch[i, j] > 127:
                kirsch[i, j] = 255
            else:
                kirsch[i, j] = 0
    return kirsch


# 拉普拉斯卷积核
kernel_Laplacian_1 = np.array([
    [0, 1, 0],
    [1, -4, 1],
    [0, 1, 0]])
kernel_Laplacian_2 = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]])
# 下面两个卷积核不具有旋转不变性
kernel_Laplacian_3 = np.array([
    [2, -1, 2],
    [-1, -4, -1],
    [2, 1, 2]])
kernel_Laplacian_4 = np.array([
    [-1, 2, -1],
    [2, -4, 2],
    [-1, 2, -1]])
# 5*5 LoG卷积模板
kernel_LoG = np.array([
    [0, 0, -1, 0, 0],
    [0, -1, -2, -1, 0],
    [-1, -2, 16, -2, -1],
    [0, -1, -2, -1, 0],
    [0, 0, -1, 0, 0]])
# 卷积
output_1 = cv2.filter2D(image, -1, kernel_Prewitt_x)
output_2 = cv2.filter2D(image, -1, kernel_Sobel_x)
output_3 = cv2.filter2D(image, -1, kernel_Prewitt_x)
output_4 = cv2.filter2D(image, -1, kernel_Laplacian_1)
output_5 = kirsch(image)
# 显示锐化效果
image = cv2.resize(image, (800, 600))
output_1 = cv2.resize(output_1, (800, 600))
output_2 = cv2.resize(output_2, (800, 600))
output_3 = cv2.resize(output_3, (800, 600))
output_4 = cv2.resize(output_4, (800, 600))
output_5 = cv2.resize(output_5, (800, 600))
cv2.imshow('Original Image', image)
cv2.imshow('Prewitt Image', output_1)
cv2.imshow('Sobel Image', output_2)
cv2.imshow('Prewitt Image', output_3)
cv2.imshow('Laplacian Image', output_4)
cv2.imshow('kirsch Image', output_5)
# 停顿
if cv2.waitKey(0) & 0xFF == 27:
    cv2.destroyAllWindows()

结果如下:

原图:

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

kirsch算子:

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

 Laplacian算子:

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

Sobel算子:

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

Prewitt算子: 

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

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

import cv2

# 加载图像
image = cv2.imread('number.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()

结果如下:

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

总结

简单实现了几种常用的传统边缘检测算子,认识了卷积的定义及运算方法,实现了二维卷积运算。

你可能感兴趣的:(NNDL 实验六 卷积神经网络(1)卷积)