Pytorch总结十之 卷积神经网络结构原理剖析

Pytorch总结十之卷积神经网络结构原理剖析

讲述关于神经网络的基本结构、数学原理

  • 二维卷积层
  • 填充和步幅
  • 多输入通道和多输出通道
  • 池化层

1.二维卷积层

卷积神经⽹络(convolutional neural network)是含有卷积层(convolutional layer)的神经⽹络。本章中介绍的卷积神经⽹络均使⽤最常⻅的⼆维卷积层。它有⾼和宽两个空间维度,常⽤来处理图像数据。

1.1 二维互相关运算

虽然卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使⽤更加直观的互相关(cross-correlation)运算。在⼆维卷积层中,⼀个⼆维输⼊数组和⼀个⼆维核(kernel)数组通过互相关运算输出⼀个⼆维数组。 我们⽤⼀个具体例⼦来解释⼆维互相关运算的含义。如下图所示,输⼊是⼀个⾼和宽均为3的⼆维数组。我们将该数组的形状记为 或(3,3)。核数组的⾼和宽分别为2。该数组在卷积计算中⼜称卷积核或过滤器(filter)。卷积核窗⼝(⼜称卷积窗⼝)的形状取决于卷积核的⾼和宽,即 2 x 2。图中的阴影部分为第⼀个输出元素及其计算所使⽤的输⼊和核数组元素:0 x 0 + 1 x 1 + 3 x 2 + 4 x 3 = 19
在这里插入图片描述
在⼆维互相关运算中,卷积窗⼝从输⼊数组的最左上⽅开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。
在这里插入图片描述
下⾯我们将上述过程实现在 corr2d 函数⾥。它接受输⼊数组 X 与核数组 K ,并输出数组 Y

import torch
from torch import nn
print('torch:',torch.cuda.get_device_name(0),'  GPU:',torch.cuda.is_available())

def corr2d(X,K):
    h,w=K.shape
    Y=torch.zeros((X.shape[0]-h+1,X.shape[1]-w+1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i,j]=(X[i:i+h,j:j+w]*K).sum()
    return Y

#构造输入和卷积核来验证二维互相关的输出
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
K = torch.tensor([[0, 1], [2, 3]])
Y = corr2d(X, K)
print('Y = ',Y)

在这里插入图片描述

1.2 二维卷积层

  • ⼆维卷积层将输⼊和卷积核做互相关运算,并加上⼀个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
  • 下⾯基于 corr2d 函数来实现⼀个⾃定义的⼆维卷积层。在构造函数 __init__ ⾥我们声明 weight和 bias 这两个模型参数。前向计算函数 forward 则是直接调⽤ corr2d 函数再加上偏差。
#二维卷积层
class Conv2D(nn.Module): #继承
    def __init__(self,kernel_size):
        super(Conv2D,self).__init__()  #可以简化执行父类的方法
        self.weight=nn.Parameter(torch.randn(kernel_size))
        self.bias=nn.Parameter(torch.randn(1))
    def forward(self,x):
        return corr2d(x,self.weight)+self.bias

在这里插入图片描述

1.3 图像中物体边缘检测

边缘检测,即找到像素变化的位置,首先构造一张6 x 8的图像(即⾼和宽分别为6像素和8像素的图像),中间四列为黑(0),其余为白(1)。

#边缘检测
X=torch.ones(6,8)  #构造图像
X[:,2:6]=0
print('X = ',X)

#构造卷积核
K=torch.tensor([[1,-1]])

#互相关:
Y=corr2d(X,K)
print('Y = ',Y)

可以看出,我们将从⽩到⿊的边缘和从⿊到⽩的边缘分别检测成了1和-1。其余部分的输出全是0
我们可以看出,卷积层可通过重复使⽤卷积核有效地表征局部空间。
在这里插入图片描述

1.4 通过数据学习核数组

最后我们来看⼀个例⼦,它使⽤物体边缘检测中的输⼊数据 X 和输出数据 Y 来学习我们构造的核数组
K 。我们⾸先构造⼀个卷积层,其卷积核将被初始化成随机数组。接下来在每⼀次迭代中,我们使⽤平
⽅误差来⽐较 Y 和卷积层的输出,然后计算梯度来更新权重。

#学习核数组
#构造一个核数组形状是(1,2)的二维卷积层
conv2d=Conv2D(kernel_size=(1,2))
step=20 #步幅
lr=0.01 #学习率
for i in range(step):
    Y_hat=conv2d(X)
    l=((Y_hat-Y)**2).sum()
    l.backward()
    # 梯度下降
    conv2d.weight.data-=lr*conv2d.weight.grad
    conv2d.bias.data-=lr*conv2d.bias.grad
    #梯度清零
    conv2d.weight.grad.fill_(0)
    conv2d.bias.grad.fill_(0)
    if(i+1)%5==0:
        print('Step %d, loss %.3f' % (i + 1, l.item()))

#查看学习参数
print("weight: ", conv2d.weight.data)
print("bias: ", conv2d.bias.data)

可以看到,20次迭代后误差已经降到了⼀个⽐较⼩的值。现在来看⼀下学习到的卷积核的参数。
在这里插入图片描述
可以看到,学到的卷积核的权᯿参数与我们之前定义的核数组 K 较接近,⽽偏置参数接近0。

1.5 互相关与卷积运算

  • 实际上,卷积运算与互相关运算类似。为了得到卷积运算的输出,只需将核数组左右翻转并上下翻转,再与数组作互相关运算,输出是不同的。
  • 使用互相关运算学习出的核数组,再进行上下、左右翻转依然可以得出卷积的输出结果。

1.6 特征图与感受野

在这里插入图片描述

  • ⼆维卷积层输出的⼆维数组可以看作是输⼊在空间维度(宽和⾼)上某⼀级的表征,也叫特征图(feature map)。影响元素 的前向计算的所有可能输⼊区域(可能⼤于输⼊的实际尺⼨)叫做 x 的感受野(receptive field)。
  • 以上为例,输⼊中阴影部分的四个元素是输出中阴影部分对应元素的感受野。
  • 我们将图中形状为2 x 2 的输出记为Y ,并考虑⼀个更深的卷积神经⽹络:将Y 与另⼀个形状为2 x 2 的核数组做互相关运算,输出单个元素z 。那么,zY上的感受野包括 Y的全部四个元素,在输⼊x 上的感受野包括其中全部9个元素。可⻅,我们可以通过更深的卷积神经⽹络使特征图中单个元的感受野变得更加⼴阔,从⽽捕捉输⼊上更⼤尺⼨的特征。

2.填充和步幅

在这里插入图片描述

2.1 填充

在这里插入图片描述

  • 卷积神经⽹络经常使⽤奇数⾼宽的卷积核,如1、3、5 和 7,所以两端上的填充个数相等。对任意的⼆维数组 X ,设它的第 i ⾏第 j 列的元素为 X[i,j] 。当两端上的填充个数相等,并使输⼊和输出具有相同的⾼和宽时,我们就知道输出 Y[i,j] 是由输⼊以 X[i,j] 为中⼼的窗⼝同卷积核进⾏互相关计算得到的。
  • 下⾯的例⼦⾥我们创建⼀个⾼和宽为3的⼆维卷积层,然后设输⼊⾼和宽两侧的填充数分别为1。给定⼀个⾼和宽为8的输⼊,我们发现输出的⾼和宽也是8
#填充
#定义一个函数来计算卷积层,它对输入和输出作相应的升维和降维
def comp_conv2d(conv2d,X):
    #(1,1)代表批量大小和通道数
    X=X.view((1,1)+ X.shape)
    Y=conv2d(X)
    return Y.view(Y.shape[2:]) #排除不关心的前两维:批量和通道
conv2d=nn.Conv2d(in_channels=1,out_channels=1,kernel_size=3,padding=1)
X=torch.rand(8,8)
s = comp_conv2d(conv2d,X).shape
print(' comp_conv2d(conv2d,X).shape = ',s)

#当卷积核的⾼和宽不同时,我们也可以通过设置⾼和宽上不同的填充数使输出和输⼊具有相同的⾼和宽。
# 使⽤⾼为5、宽为3的卷积核。在⾼和宽两侧的填充数分别为2和1
conv2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=(5, 3), padding=(2, 1))
s1 = comp_conv2d(conv2d, X).shape
print(' comp_conv2d(conv2d,X).shape = ',s1)

在这里插入图片描述

2.2 步幅

卷积窗⼝从输⼊数组的最左上⽅开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。我们将每次滑动的⾏数和列数称为步幅(stride)。
在这里插入图片描述

#令高和宽上的步幅均为2,从而使输入的高和宽减半
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
print(comp_conv2d(conv2d, X).shape)
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride= (3, 4))
print(comp_conv2d(conv2d, X).shape)

在这里插入图片描述
在这里插入图片描述

3.多输入通道和多输出通道

将介绍含多个输⼊通道或多个输出通道的卷积核。

3.1 多输入通道

在这里插入图片描述
在这里插入图片描述
接下来我们实现含多个输⼊通道的互相关运算。我们只需要对每个通道做互相关运算,然后通过add_n 函数来进⾏累加。

import sys
sys.path.append("..")
import d2lzh_pytorch as d2l

def corr2d_multi_in(X,K):
    #沿着X和K的第0维分别计算再相加
    res=d2l.corr2d(X[0,:,:],K[0,:,:])
    for i in range(1,X.shape[0]):
        res+=d2l.corr2d(X[i,:,:],K[i,:,:])
    return res

#构造图5.4中的输⼊数组 X 、核数组 K 来验证互相关运算的输出
X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
 [[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]], [[1, 2], [3, 4]]])
print(corr2d_multi_in(X, K))

output:
在这里插入图片描述

3.2 多输出通道

在这里插入图片描述
下⾯我们实现⼀个互相关运算函数来计算多个通道的输出。

#多输出
def corr2d_multi_in_out(X, K):
    # 对K的第0维遍历,每次同输⼊X做互相关计算。所有结果使⽤stack函数合并在⼀起
    return torch.stack([corr2d_multi_in(X, k) for k in K])

#核数组 K 同 K+1 ( K 中每个元素加⼀)和 K+2 连结在⼀起来构造⼀个输出通道数为3的卷积核
K = torch.stack([K, K + 1, K + 2])
print(K.shape) # torch.Size([3, 2, 2, 2])
#对输⼊数组 X 与核数组 K 做互相关运算。此时的输出含有3个通道。其中第⼀个通道的结果与
#之前输⼊数组 X 与多输⼊通道、单输出通道核的计算结果⼀致。
print(corr2d_multi_in_out(X, K))

output:
在这里插入图片描述

4.池化层

池化(pooling)层,它的提出是为了缓解卷积层对位置的过度敏感性。

4.1 二维最大池化层和平均池化层

在这里插入图片描述

现在我们将卷积层的输出作为 最⼤池化的 2 x 2输⼊。设该卷积层输⼊是 X 、池化层输出为 Y 。⽆论是 X[i, j]X[i, j+1] 值不同,还是 X[i,j+1]X[i, j+2] 不同,池化层输出均有 Y[i, j]=1 。也就是说,使⽤2 x 2 最⼤池化层时,只要卷积层识别的模式在⾼和宽上移动不超过⼀个元素,我们依然可以将它检测出来。
把池化层的前向计算实现在 pool2d 函数⾥。它跟corr2d 函数⾮常类似,唯⼀的区别在计算输出 Y 上。

#卷积层
def pool2d(X,pool_size,mode='max'):
    X=X.float()
    p_h,p_w=pool_size
    Y=torch.zeros(X.shape[0] - p_h + 1,X.shape[1] - p_w + 1)
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if mode=='max':
                Y[i,j]=X[i:i+p_h,j:j+p_w].max()
            elif mode=='avg':
                Y[i,j]=X[i:i+p_h,j:j+p_w].mean()
    return Y
#构造输⼊数组 X 来验证⼆维最⼤池化层的输出。
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
print(pool2d(X, (2, 2)))
#平均池化层:
print(pool2d(X, (2, 2), 'avg'))

output:
在这里插入图片描述

4.2 填充和步幅

同卷积层⼀样,池化层也可以在输⼊的⾼和宽两侧的填充并调整窗⼝的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的⼯作机制⼀样。我们将通过 nn 模块⾥的⼆维最⼤池化层MaxPool2d 来演示池化层填充和步幅的⼯作机制。我们先构造⼀个形状为(1, 1, 4, 4)的输⼊数据,前两个维度分别是批量和通道。

X = torch.arange(16, dtype=torch.float).view((1, 1, 4, 4))
print('X = ',X)

output:

X =  tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]]]])

默认情况下, MaxPool2d 实例⾥步幅和池化窗⼝形状相同。下⾯使⽤形状为(3, 3)的池化窗⼝,默认获
得形状为(3, 3)的步幅。

pool2d = nn.MaxPool2d(3)
print('pool2d(X):',pool2d(X))

output:

pool2d(X): tensor([[[[10.]]]])

⼿动指定步幅和填充:

pool2d = nn.MaxPool2d(3, padding=1, stride=2) #手动指定步幅
print('pool2d(X):',pool2d(X))

output:

pool2d(X): tensor([[[[ 5.,  7.],
          [13., 15.]]]])

也可以指定⾮正⽅形的池化窗⼝,并分别指定⾼和宽上的填充和步幅:

pool2d = nn.MaxPool2d((2, 4), padding=(1, 2), stride=(2, 3))  #非正方形
print('pool2d(X):',pool2d(X))

output:

pool2d(X): tensor([[[[ 1.,  3.],
          [ 9., 11.],
          [13., 15.]]]])

4.3 多通道

在处理多通道输⼊数据时,池化层对每个输⼊通道分别池化,⽽不是像卷积层那样将各通道的输⼊按通
道相加。这意味着池化层的输出通道数与输⼊通道数相等。下⾯将数组 XX+1 在通道维上连结来构
造通道数为2的输⼊。

#池化多通道:
X = torch.cat((X, X + 1), dim=1)
print('X = ',X)

pool2d = nn.MaxPool2d(3, padding=1, stride=2)
print('池化后:',pool2d(X))

池化后,我们发现输出通道数仍然是2。

X =  tensor([[[[ 0.,  1.,  2.,  3.],
          [ 4.,  5.,  6.,  7.],
          [ 8.,  9., 10., 11.],
          [12., 13., 14., 15.]],

         [[ 1.,  2.,  3.,  4.],
          [ 5.,  6.,  7.,  8.],
          [ 9., 10., 11., 12.],
          [13., 14., 15., 16.]]]])
池化后: tensor([[[[ 5.,  7.],
          [13., 15.]],

         [[ 6.,  8.],
          [14., 16.]]]])

猜你喜欢

转载自blog.csdn.net/yohnyang/article/details/127046473