[2022-10-25]神经网络与深度学习第4章 - 卷积神经网络(part 2)

contents

  • 神经网络与深度学习第4章 - 卷积神经网络(part 2) - 算子
    • 写在开头
    • 卷积神经网络的基础算子
      • 卷积算子
        • 多通道卷积
          • 卷积过程
          • 输入通道、输出通道
        • 多通道卷积层算子
          • 代码实现
          • 初始化类部分
          • 参数准备部分
          • 前向传播部分
          • 使用pytorch实现
          • 比较自定义算子和框架中的算子
        • 卷积算子的参数量和计算量
      • 汇聚层(池化层)算子
        • 代码实现
          • 汇聚层算子实现
          • 使用pytorch实现
          • 比较自定义算子和框架中的算子
        • 汇聚层的参数量和计算量
    • 一些小题
      • 简单翻译
    • 写在最后

神经网络与深度学习第4章 - 卷积神经网络(part 2) - 算子

写在开头

神经网络离不开各种用于处理的算子,卷积神经网络也不例外。本次实验我们将对卷积的算子进行了解和实现。

卷积神经网络的基础算子

卷积算子

我们在前面所了解的、包括前面我们进行研究的,都只是单一通道的卷积。但是在现实中,卷积通常需要使用多通道的卷积,比如现实中的图像是彩色的,因此有RGB三通道。为了提取不同种类的特征,我们使用多个卷积核进行特征提取。

多通道卷积

相较于单通道,多通道卷积拥有更多输入。通过将每个通道分别进行卷积然后进行相加和,得到最终的输出。

卷积过程

卷积过程我们在前面已经介绍过了,多通道和的卷积过程一致,无非就是多了几个通道,并且在每个通道上都有对应的卷积核。通俗一点可以用下图理解:
[2022-10-25]神经网络与深度学习第4章 - 卷积神经网络(part 2)_第1张图片

输入通道、输出通道

这两个概念其实看字面就可以理解了。输入通道是指输入内容的矩阵个数。以三色图片为例,图片可以抽象为有三种颜色的矩阵,因此其应当有三个通道。输出通道是指输出内容的矩阵个数。这两个参数设置后的计算过程是什么样的呢?在卷积过程中,我们由于设置好了输出通道数 o u t c h a n n e l s out_channels outchannels,因此最终输出的“线路”会有 o u t c h a n n e l s out_channels outchannels条;在每条线路上,由于有 i n c h a n n e l s in_channels inchannels个通道存在,因此我们需要有 i n c h a n n e l s in_channels inchannels个卷积核,对输入进行卷积操作。

多通道卷积层算子

代码实现

在 hw6中,我们已经给出了自造轮子的多通道卷积层算子实现,但是写的代码和pytorch不是很能对应上,在这边我们进行修改并加上注释:

初始化类部分

由于我们还没学习空洞卷积等内容,这边我们先只定义简单的卷积核:

class Conv2Dd(LayerBase):
    def __init__(
                self,
                in_channels     : int,                  # 输入通道数
                out_channels    : int,                  # 输出通道数
                kernel_size     : tuple,                # 卷积核大小
                padding         : str       = 'same',   # 卷积填充
                stride          : int       = 1         # 卷积步长
                ):
        self.in_channels    = in_channels
        self.out_channels   = out_channels
        self.kernel_size    = kernel_size if isinstance(kernel_size, tuple) else (kernel_size, kernel_size) # 防止输入问题
        self.padding        = padding
        self.stride         = stride
参数准备部分

在卷积设置完毕后,我们需要进行参数初始化的操作。由于前面造的轮子中将优化器初始化也放入了这个过程,因此代码如下:

def setup(self, optimizer):
        kernel_h, kernel_w = self.kernel_size # 卷积核长宽
        channels = self.in_channels # 通道数
        limit = 1 / np.sqrt(np.prod(self.filter_shape)) # 计算总元素乘积开方,划定随机范围
        self.weight  = np.random.uniform(-limit, limit, size=(self.out_channels, channels, kernel_h, kernel_w)) # 卷积核数组
        self.bias = np.zeros((self.kernel_size, 1)) # 卷积偏置值
        self.weight_opt  = copy.copy(optimizer) # 权重优化器
        self.bias_opt = copy.copy(optimizer) # 偏置优化器
前向传播部分

前向传播部分即从输入进行卷积得到输出的过程。其中的image2column已经放在 hw6中。代码如下:

def forward(self, X):
        batch_size, channels, height, width = X.shape # 输入大小
        self._input = X # 保留输入值用于反向传播
        self.X_col = image2column(X, self.in_channels, self.stride, self.padding) # 将图像转换为列向量,用于计算输出和保留作为反向传播参数
        W_col = self.W.reshape((self.in_channels, -1)) # 将卷积核转换为行向量
        output = W_col.dot(self.X_col) + self.bias # 计算卷积结果
        output = output.reshape((self.in_channels, ) + (batch_size, )) # 将卷积结果重组为二维
        return output.transpose(3,0,1,2) # 由于在image2column中batch_size被移动到最后,输出需要重新移动到最前
使用pytorch实现

使用pytorch进行实现,代码和上面类似,只是不需要自己写梯度的计算了。代码如下:

class Conv2D(torch.nn.Module):
	def __init__(
                self,
                in_channels     : int,                  # 输入通道数
                out_channels    : int,                  # 输出通道数
                kernel_size     : tuple,                # 卷积核大小
                padding         : str       = 'same',   # 卷积填充
                stride          : int       = 1         # 卷积步长
                ):
        self.in_channels    = in_channels
        self.out_channels   = out_channels
        self.kernel_size    = kernel_size if isinstance(kernel_size, tuple) else (kernel_size, kernel_size) # 防止输入问题
        self.padding        = padding
        self.stride         = stride
        kernel_h, kernel_w = self.kernel_size # 卷积核长宽
        channels = self.in_channels # 通道数
        limit = 1 / np.sqrt(np.prod(self.filter_shape)) # 计算总元素乘积开方,划定随机范围
        self.weight  = np.random.uniform(-limit, limit, size=(self.out_channels, channels, kernel_h, kernel_w)) # 卷积核数组
        self.bias = np.zeros((self.kernel_size, 1)) # 卷积偏置值

def forward(self, X):
        batch_size, channels, height, width = X.shape # 输入大小
        X_col = image2column(X, self.in_channels, self.stride, self.padding) # 将图像转换为列向量,用于计算输出和保留作为反向传播参数,这边将函数中所有np改为torch即可
        W_col = self.W.reshape((self.in_channels, -1)) # 将卷积核转换为行向量
        output = W_col.dot(X_col) + self.bias # 计算卷积结果
        output = output.reshape((self.in_channels, ) + (batch_size, )) # 将卷积结果重组为二维
        return output.transpose(3,0,1,2) # 由于在image2column中batch_size被移动到最后,输出需要重新移动到最前
比较自定义算子和框架中的算子

由于自定义算子写的比较完善,所以和框架中的算子并无二异。但是本次代码实现的卷积是最简单版本,没有膨胀系数等参数,且还没有进行反向传播的编写。

卷积算子的参数量和计算量

还是以这张图为例,可见:
[2022-10-25]神经网络与深度学习第4章 - 卷积神经网络(part 2)_第2张图片
输入有 n n n个,卷积核为 k × k k \times k k×k,每个通道都有一个偏置值,输出为 m m m通道。因此,总共的参数量:
P a r a m s t o t a l = P a r a m s k e r n e l s + P a r a m s b i a s = k × k × n × m + m Params_{total} = Params_{kernels} + Params_{bias} = k \times k \times n \times m + m Paramstotal=Paramskernels+Paramsbias=k×k×n×m+m
计算量存在于卷积过程和加和过程中,因此,计算量:
C t o t a l = C C o n v + C b i a s = ( a − k + 2 p s + 1 ) × ( b − k + 2 p s + 1 ) × ( 2 × k × k − 1 ) × m × ( m − 1 ) × n + ( n − 1 ) × m C_{total} = C_{Conv}+C_{bias} = (\frac{a - k + 2p}{s} + 1) \times (\frac{b - k + 2p}{s} + 1) \times (2 \times k \times k -1) \times m \times (m-1)\times n + (n -1) \times m Ctotal=CConv+Cbias=(sak+2p+1)×(sbk+2p+1)×(2×k×k1)×m×(m1)×n+(n1)×m

汇聚层(池化层)算子

在 hw6中,我们已经给出了自造轮子的池化层算子实现,但是写的代码和pytorch不是很能对应上,在这边我们进行修改并加上注释:

代码实现

由于不同的池化计算方法略有不同,这里编写一个池化基类,用于统一调用池化。

汇聚层算子实现

汇聚层基类如下,含有基本的初始化和前向操作:

class PoolingBase(LayerBase):
    def __init__(self, kernel_size, stride=1, padding=0):
        self.kernel_size 	= kernel_size 	# 池化核大小
        self.stride 		= stride if isinstance(stride, tuple) else (stride, stride)	# 池化步长
        self.padding 		= padding		# 池化填充

    def forward(self, X):
        self._input = X # 保留输入用于反向传播
        batch_size, channels, height, width = X.shape
		
        out_height = int((height- self.pool_shape[0]) / self.stride + 1)
        out_width = int((width - self.pool_shape[1]) / self.stride + 1)

        X = X.reshape(batch_size * channels, 1, height, width)
        X_col = image2column(X, self.kernel_size, self.stride, self.padding)
        output = self._sub_forward(X_col)
        output = output.reshape(out_height, out_width, batch_size, channels)
        output = output.transpose(2, 3, 0, 1)
        return output

下面是平均池化:

class AveragePool2D(PoolingBase):
    def _sub_forward(self, X_col):
        output = np.mean(X_col, axis=0)
        return output

下面是最大池化:

class MaxPool2D(PoolingBase):
    def _sub_forward(self, X_col):
        arg_max = np.argmax(X_col, axis=0).flatten()
        output = X_col[arg_max, range(arg_max.size)]
        self.cache = arg_max
        return output
使用pytorch实现

使用朋友torch实现时,只需要将自己所用的用于梯度追踪的内容删去,并将np改为torch即可:

class PoolingBase(torch.nn.Module):
    def __init__(self, kernel_size, stride=1, padding=0):
        self.kernel_size 	= kernel_size 	# 池化核大小
        self.stride 		= stride if isinstance(stride, tuple) else (stride, stride)	# 池化步长
        self.padding 		= padding		# 池化填充

    def forward(self, X):
        batch_size, channels, height, width = X.shape
        out_height = int((height- self.pool_shape[0]) / self.stride + 1)
        out_width = int((width - self.pool_shape[1]) / self.stride + 1)

        X = X.reshape(batch_size * channels, 1, height, width)
        X_col = image2column(X, self.kernel_size, self.stride, self.padding)
        output = self._sub_forward(X_col)
        output = output.reshape(out_height, out_width, batch_size, channels)
        output = output.transpose(2, 3, 0, 1)
        return output

class MaxPool2D(PoolingBase):
    def _sub_forward(self, X_col):
        arg_max = torch.argmax(X_col, axis=0).flatten()
        output = X_col[arg_max, range(arg_max.size)
        return output

class AveragePool2D(PoolingBase):
    def _sub_forward(self, X_col):
        output = np.mean(X_col, axis=0)
        return output
比较自定义算子和框架中的算子

由于自定义算子写的比较完善,所以和框架中的算子并无二异。

汇聚层的参数量和计算量

  • 由于汇聚层中没有参数,所以参数量为0;
  • 最大汇聚中,没有乘加运算,所以计算量为0,
  • 平均汇聚中,输出特征图上每个点都对应了一次求平均运算。

一些小题

简单翻译

翻译来源:传送门
[2022-10-25]神经网络与深度学习第4章 - 卷积神经网络(part 2)_第3张图片
中文:

卷积样例: 下面的图是一个卷积层动态进行运算的样例。因为3D的输入非常难搞可视化,所有的矩阵(蓝色的输入矩阵,绿色的输出矩阵)通过把每个通道都单独拿出来(按照文中所谓“深度方向”)进行可视化。输入的矩阵大小(W宽,H高,D通道数): W 1 = 5 , H 1 = 5 , D 1 = 3 W_1=5, H_1=5, D_1=3 W1=5,H1=5,D1=3,卷积层参数(K输出通道数,F卷积核边长,S卷积步长,P填充): K = 2 , F = 3 , S = 2 , P = 1 K=2, F=3, S=2, P=1 K=2,F=3,S=2,P=1。对这几个变量的解释就是前面括号里面的内容。由此我们能够得到输出矩阵的最终大小是 ( 5 − 3 + 2 ) / 2 + 1 = 3 (5-3+2)/2+1=3 (53+2)/2+1=3。另外,注意我们对输入进行了宽度为1的填充(在外面填充一圈0)。下面的可视化迭代了所有的输出值(绿色的部分),并且展示了这些值是咋来的:将蓝色的输入矩阵通过红色的卷积核进行卷积和加和,然后再用偏置值给算出来的结果统一加上。
[2022-10-25]神经网络与深度学习第4章 - 卷积神经网络(part 2)_第4张图片

写在最后

通过本次实验,我们了解到了卷积神经网络中至关重要的两个算子——卷积、池化算子。通过编程实现这两个算子和计算这两个算子所需要的计算资源,我们更加深刻地了解到了卷积神经网络进行卷积、池化的原理、过程和相较于前馈神经网络而言节省计算量且能够自动提取深层特征的特点。
本次的实验暂时没有放上反向传播的轮子代码,因为反向传播又是一个需要细讲的内容喽。

你可能感兴趣的:([DL]神经网络与深度学习)