介绍ResNet之前,首先介绍一下批量归一化(BatchNormalization)。
BatchNormalization
通常来说,输入数据标准化预处理对于浅层模型就足够有效了,随着模型训练的进行,当每层的参数更新时,靠近输出层的输出比较难出现剧烈变化。但是对深层神经网络来说,即使输入数据已做了标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种数值的不稳定性难以训练出较深的网络模型。
批量归一化层在模型训练时,利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出数值更加稳定。
全连接层批量归一化
假设一个由 m 个样本组成的小批量 β = { x 1, … , x m}, 首先对 β 个样本求均值和方差:
其中的平方计算是按元素求平方。接下来,使用按元素开方和按元素除法对 x(i) 标准化:
这里ε > 0,是一个很小的常数,保证分母大于0。在上面标准化的基础上,批量归一化层引入了两个可学习的模型参数,拉伸 scale 和偏移 bias 。这两个参数分别与 x(i) 做按元素乘加计算:
至此,得到了 x(i) 的批量归一化输出 y(i),同时可学习的 scale 和 bias 参数保留了不对x(i) 做批量归一化的可能,即,如果批量归一化无益,学出的模型可以不使用批量归一化。
卷积层批量归一化
对卷积层来说,批量归一化发生在卷积计算后,激活函数前。如果卷积计算输出多个通道,则需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的 scale 和 bias 参数。BN层经过等价变换后,可理解为对每一个通道分别做乘加运算。假设小批量中有 m 个样本,卷积计算输出的高和宽分别为 h 和 w。我们需要对该通道中 m x h x w 个元素同时做批量归一化,并使用相同的均值、方差、scale和bias。
训练和推理时的批量归一化
使用批量归一化训练时,可以将批量的大小设置的大一点,从而使批量内样本的均值和方差计算的更为准确。训练时,每个批量的数据都有独立的均值和方差。而在推理时,单个样本的上输入不应取决于批量归一化所需要的随机小批量中的均值和方差。
常用的解决方法是通过移动平均估算整个训练数据集的样本均值和方差,并在推理时使用它们得到确定的输出。
因此,BN层在训练模式下和预测模式下会得到不同的计算结果。
BN层Pytorch实现:
def batch_norm(is_training, X, scale, biass, moving_mean, moving_var, eps, momentum):
if not is_training:
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
mean = X.mean(dim = 0)
var = ((X - mean) ** 2).mean(dim = 0)
else:
mean = X.mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
X_hat = (X - mean) / torch.sqrt(var + eps)
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = scale* X_hat + bias
return Y, moving_mean, moving_var
class BatchNorm(nn.Module):
def __init__(self, num_fratures, num_dims):
super(BatchNorm, self).__init__()
if num_dims == 2:
shape = (1, num_fratures)
else:
shape = (1, num_fratures, 1, 1)
self.scale = nn.Parameter(torch.ones(shape))
self.bias = nn.Parameter(torch.zeros(shape))
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.zeros(shape)
def forward(self, X):
if self.moving_mean.device != X.device:
self.moving_mean = self.moving_mean.to(X.device)
self.moving_var = self.moving_var.to(X.device)
Y, self.moving_mean, self.moving_var = batch_norm(self.training, X, self.scale, self.bias,
self.moving_mean, self.moving_var, eps=1e-5, momentum=0.9)
return Y
ResNet
对神经网络添加新的层,充分训练后,理论上原模型解的空间只是新模型解的子空间。也就是说,如果我们能将新添加的层训练成恒等映射 f(x) = x,新模型和原模型将同样有效。由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。然而在事件中,添加过多的层后,训练误差往往不降反升。即使利用批量归一化带来的数值稳定性使训练深层模型更加容易,该问题仍然存在。针对这一问题,何恺明等人提出了残差网络ResNet。
ResNet由残差块构建,聚焦于神经网络局部。设输入为x,假设我们希望学出的理想映射为f(x),作为激活函数的输入。左图中的部分需要直接拟合出该映射f(x),而右图虚线框中的部分则需要拟合出有关恒等映射的残差映射f(x) - x。残差映射在实际中往往更容易优化。当理想映射极接近与恒等映射时,残差块也易于捕捉恒等映射的席位波动。
ResNet沿用了VGG全3x3卷积层的设计。残差块有两个有相同输出通道数的3x3卷积,每个卷积层后借一个BN层和ReLU层。然后将输入跳过这两个卷积运算后直接加在最后的ReLU激活函数前。这要求两个卷积层的输出与输入形状一致,引入额外的1x1卷积来改变输出形状。
Pytorch实现:
class Residual(nn.Module):
def __init__(self, in_channels, out_channels, use_1x1conv=False, stride=1):
super(Residual, self).__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, stride=stride)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
# change input shape
if use_1x1conv:
self.conv3 = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(out_channels)
self.bn2 = nn.BatchNorm2d(out_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
return F.relu(Y + X)
ResNet前两层与GoogleNet一样,不同之处在于ResNet每个卷积层后增加的BN层。ResNet使用4个由残差块组成的模块,每个模块使用若干个同样输出通道的残差块。下面为该模块的Pytorch实现:
def resnet_block(in_channels, out_channels, num_residuals, first_block=False):
if first_block:
assert in_channels == out_channels
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(in_channels, out_channels, use_1x1conv=True, stride=2))
else:
blk.append(Residual(out_channels, out_channels))
return nn.Sequential(*blk)
将每个残差块加入到ResNet中,最后加上全局平均池化和全连接层输出。这里每个模块有4个卷积层,加上最开始的卷积层和最后的全连接层,一共18层。因此这个模型通常被称之为ResNet-18。通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet152。
ResNet通过跨层的数据通道,能够训练出更深更有效的深度神经网络,从而深刻影响了后来的深度神经网络设计。
def ResNet-18():
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
net.add_module('resnet_block1', resnet_block(64, 64, 2, first_block=True))
net.add_module('resnet_block2', resnet_block(64, 128, 2))
net.add_module('resnet_block3', resnet_block(128, 256, 2))
net.add_module('resnet_block4', resnet_block(256, 512, 2))
net.add_module('global_avg_pool', layer.GlobalAvgPool2d())
net.add_module('fc', nn.Sequential(layer.FlattenLayer(), nn.Linear(512, 10)))
return net
DenseNet
ResNet中的跨层链接设计引申出了数个后续工作,其中之一:稠密连接网络(DenseNet)。它与ResNet的主要区别为:在跨层连接上,不同于ResNet中将输入与输出相加,DenseNet在通道维度上连结输入与输出。
DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输入和输出是如何连结的,后者则用来控制通道数,使之不过大。
稠密块
DensNet使用了BN、ReLU和Conv结构组成稠密块,每块使用相同的输出通道数。但在前向计算时,将每块的输入和输出在通道维上连结。
def conv_block(in_channels, out_channels):
blk = nn.Sequential(nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
return blk
class DenseBlock(nn.Module):
def __init__(self, num_convs, in_channels, out_channels):
super(DenseBlock, self).__init__()
net = []
for i in range(num_convs):
in_c = in_channels + i * out_channels
net.append(conv_block(in_c, out_channels))
self.net = nn.ModuleList(net)
self.out_channels = in_channels + num_convs * out_channels
def forward(self, X):
for blk in self.net:
Y = blk(X)
X = torch.cat((X, Y), dim=1)
return X
过渡层
每个稠密块都会带来通道数的增加,使用过多则会带来过于复杂的模型。过渡层用来控制模型复杂度,借助1x1卷积层和stride为2的平均池化层分别减少通道数和宽高,进一步降低模型复杂度:
def transition_block(in_channels, out_channels):
blk = nn.Sequential(
nn.BatchNorm2d(in_channels),
nn.ReLU(),
nn.Conv2d(in_channels, out_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2)
)
return blk
DenseNet Pytorch实现:
def DenseNet():
net = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
for i, num_convs in enumerate(num_convs_in_dense_blocks):
DB = DenseBlock(num_convs, num_channels, growth_rate)
net.add_module('DenseBlosk_%d' % i, DB)
num_channels = DB.out_channels
if i != len(num_convs_in_dense_blocks) - 1:
net.add_module('transition_block_%d' % i, transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2
net.add_module('BN', nn.BatchNorm2d(num_channels))
net.add_module('Relu', nn.ReLU())
net.add_module('global_avg_pool', layer.GlobalAvgPool2d())
net.add_module('fc', nn.Sequential(layer.FlattenLayer(), nn.Linear(num_channels, 10)))