查看原文
其他

【强基固本】一网打尽CNN前向和反向 — 池化、padding、dropout

“强基固本,行稳致远”,科学研究离不开理论基础,人工智能学科更是需要数学、物理、神经科学等基础学科提供有力支撑,为了紧扣时代脉搏,我们推出“强基固本”专栏,讲解AI领域的基础知识,为你的科研学习提供助力,夯实理论基础,提升原始创新能力,敬请关注。

作者:知乎—皮特潘

地址:https://zhuanlan.zhihu.com/p/372659296

搞技术即要向前看,紧跟时代潮流,学点transformer、GCN等当前热门,也要“向后”看,学学基础知识,这样才能做到游刃有余。本系列文章将CNN常见组件的前向和反向传播都过一遍

01

avgpooling

平均池化层,非常常见。尤其是分类网络最后的neck部分,运用的是global average pooling(GAP),基本成为了现在网络的标配。如上图所示,蓝色代表前向传播,红色代表反向传播,通过该图可以非常直观感受其过程。
参数定义:
需要定义sliding window的大小以及stride的大小,如下:
class AvgPool2d: def __init__(self, kernel_size=2, stride=2): self.ksize = kernel_size        self.stride = stride
前向过程,将sliding window内的参数取均值;
def forward(self, x): B, C, H, W = input.shape # reshape:[1,3,16,16] => [1,3,8,2,8,2] x = x.reshape((B, C, H // self.ksize, self.ksize, W // self.ksize, self.ksize)) x = x.sum(axis=(3, 5)) # 对维度3和5求和, 获得[1,3,8,8] x = x / self.ksize ** 2 # 除以sliding window的大小    return x
反向过程,将输入梯度均分为sliding window份,然后分别输送到相关联的位置
def backward(self, grad_output): result = grad_output.repeat(self.ksize, axis=2).repeat(self.ksize, axis=3)/(self.ksize**2)    return result
pytorch对照:
pool = AvgPool2d(kernel_size=3, stride=3)x = np.random.rand(1, 1, 6, 6)y = pool.forward(x)grad = np.random.randn(*y.shape)x_grad = pool.backward(grad)print(x)print(y)
import torchpool_torch = torch.nn.AvgPool2d(kernel_size=3,stride=3)x = torch.from_numpy(x)x.requires_grad = True # 如果要验证输入的梯度,需要将x.requires_grad置为Truex.retain_grad() # 保留中间过程out = pool_torch.forward(x)out.backward(torch.from_numpy(grad))# 计算两种方式的误差print(np.linalg.norm(y - out.detach().numpy()))print(np.linalg.norm(x_grad - x.grad.detach().numpy()))
输出结果,可见存在非常微弱的误差,这和计算平台有关,验证的本numpy实现的正确性:
[[[[0.24787347 0.58462986 0.87057137 0.55470378 0.94212643 0.35796396] [0.10316125 0.89490078 0.13275839 0.18731875 0.54144992 0.09144105] [0.63152739 0.57627791 0.72482253 0.82538094 0.12523996 0.53229809] [0.14704578 0.60045983 0.00351559 0.94632261 0.83554975 0.560136 ] [0.41284019 0.8097822 0.25224494 0.62615051 0.16598724 0.75444111] [0.81084053 0.65274578 0.62837219 0.52975354 0.00869069 0.86037101]]]][[[[0.52961366 0.46199143] [0.47976078 0.58748916]]]] 1.5700924586837752e-160.0

02

maxpooling

最大池化和均值池化类似,不过是前向过程取的sliding window的最大值传播到下一层,反向过程只有最大值的位置才有梯度。如上图所示,蓝色代表前向传播,红色代表反向传播,左边标红色框的位置才有前向和反向的信息交互。
参数定义:
和平均池化类似,不过需要多记录一个index信息,也就是sliding window的最大值位置信息,用于反向传播过程。
class MaxPool2d: def __init__(self, kernel_size=(2, 2), stride=2): self.ksize = kernel_size self.stride = stride        self.index = None # 这里记录的是非最大值的index
前向传播:
def forward(self, x): N, C, H, W = x.shape out = x.reshape(N, C, H // self.stride, self.stride, W // self.stride, self.stride) out = out.max(axis=(3, 5)) self.index = out.repeat(self.ksize[0], axis=2).repeat(self.ksize[1], axis=3) != x    return out
反向传播:
def backward(self, grad_output): result = grad_output.repeat(self.ksize[0], axis=2).repeat(self.ksize[1], axis=3) result[self.index] = 0    return result
pytorch对照:
代码同上,不过是将AvgPool2d换成了MaxPool2d
输出结果,可见是和pytorch一致的:
[[[[0.41082583 0.14887217 0.63720543 0.26269506 0.26689063 0.05299152] [0.26417556 0.16590289 0.88674464 0.88322143 0.61245957 0.03046522] [0.86418915 0.77299086 0.82797095 0.9822634 0.13698056 0.69444395] [0.68113399 0.10417155 0.87924769 0.99260882 0.5379204 0.87514129] [0.98859422 0.87058761 0.8880138 0.33134971 0.75367844 0.03569214] [0.56870483 0.2410348 0.47228231 0.4095878 0.33161972 0.44368439]]]][[[[0.88674464 0.9822634 ] [0.98859422 0.99260882]]]]0.00.0


03

padding

padding非常常见,一般会应用在卷积的前面,为了调整特征图的尺度,使卷积前后的尺度一致。具体过程如上图所示。当然除了固定值的padding外,还存在ReflectionPad2d等方式,有兴趣的可以去探究一下。
参数定义:
class Padding: def __init__(self, padding_size=(1, 1, 1, 1), value=0): self.padding_size = padding_size        self.value = value
前向传播:
def forward(self, x): N, C, H, W = x.shape pad = np.ones((N, C, H + self.padding_size[2] + self.padding_size[3], W + self.padding_size[0] + self.padding_size[1])) pad *= self.value pad[..., self.padding_size[2]:H + self.padding_size[2], self.padding_size[0]:H + self.padding_size[0]] = x    return pad
反向传播:
def backward(self, grad_output):    return grad_output[..., self.padding_size[2]:grad_output.shape[2] - self.padding_size[3],self.padding_size[0]:grad_output.shape[3] - self.padding_size[1]]
pytorch对照:
padding = Padding(padding_size=(1, 1, 1, 1), value=0.0)x = np.random.randn(1,1,3,3)y = padding.forward(x)y_grad = np.random.randn(*y.shape)x_grad = padding.backward(y_grad)print(y)
import torchpadding_torch = torch.nn.ConstantPad2d(padding=(1,1,1,1), value=0.0)x = torch.from_numpy(x)x.requires_grad = Truex.retain_grad()out = padding_torch(x)out.backward(torch.from_numpy(y_grad))
print(np.linalg.norm(y-out.detach().numpy()))print(np.linalg.norm(x_grad-x.grad.detach().numpy()))
输出结果,完全一致:
[[[[ 0. 0. 0. 0. 0. ] [ 0. 2.3513949 -0.18764089 0.53695311 0. ] [ 0. -0.08604583 -0.15421848 -0.21025646 0. ] [ 0. -0.31895118 0.35480772 -0.78156063 0. ] [ 0. 0. 0. 0. 0. ]]]]0.00.0

04

dropout

dropout是非常知名的正则化手段之一,可以有效预防过拟合。其原理就是以一定的概率将神经元“失活”。所谓“失活”就是置为0,前向和反向传播都没有信息的流动,减少网络对某些连接的过度依赖。示意图如上。前面我们说过,不管是前向还是反向的过程,一般保持信息的总量不变,所以需要根据drop的概率进行修正补偿。
参数定义:
class Dropout(): def __init__(self, drop_rate=0.5, is_train=True): self.drop_rate = drop_rate self.is_train = is_train self.fix_value = 1 - drop_rate # 修正期望,保证输出值的期望不变 self.save_mask = None
前向传播:
def forward(self, x): if not self.is_train: # 当前为测试状态 return x else: # 当前为训练状态 N, m = x.shape self.save_mask = np.random.uniform(0, 1, m) > self.drop_rate # save_mask中为保留的神经元 return (x * self.save_mask) / self.fix_value
反向传播:
def backward(self, grad_output): if not self.is_train: return grad_output else: return eta * self.save_mask/self.fix_value
pytorch对照:
import torch
x = np.random.rand(2, 6)
pool_torch = torch.nn.Dropout(p=0.5)x_t = torch.from_numpy(x)x_t.requires_grad = Truey = pool_torch.forward(x_t)print('y:', y)x_t.retain_grad()eta = torch.rand_like(y)y.backward(eta)print('x_grad:',x_t.grad)
pool = Dropout(drop_rate=0.5)y = pool.forward(x)print('y:', y)z = pool.backward(eta.numpy())print('x_grad:', z)
输出结果, 由于随机的存在,结果不完全一样,但其规律是一致的:
y: tensor([[0.0000, 0.3316, 1.8085, 0.0000, 0.0000, 1.6922], [0.0000, 1.8447, 1.4564, 0.6675, 0.0000, 0.9850]], dtype=torch.float64, grad_fn=<MulBackward0>)x_grad: tensor([[0.0000, 0.6242, 0.3538, 0.0000, 0.0000, 1.2362], [0.0000, 1.6603, 0.4200, 1.8347, 0.0000, 1.8959]], dtype=torch.float64) y: [[0. 0.33158154 1.80852172 0. 0. 1.69216999] [0. 1.8446859 1.45642869 0. 0. 0.98497914]]x_grad: [[0. 0.62420836 0.35375397 0. 0. 1.23621819]        [0.         1.66027998 0.41999203 0.         0.         1.89585741]]

本文目的在于学术交流,并不代表本公众号赞同其观点或对其内容真实性负责,版权归原作者所有,如有侵权请告知删除。


“强基固本”历史文章


更多强基固本专栏文章,

请点击文章底部“阅读原文”查看


分享、点赞、在看,给个三连击呗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存