批量归一化和残差网络
批量归一化(BatchNormalization)
与对输入的标准化的对比
对输入的标准化(浅层模型)
处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。
标准化处理输入数据使各个特征的分布相近
批量归一化(深度模型)
利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。
对全连接层做批量归一化
全连接层的输出:batch_size×输出神经元个数。是对batch_size做批量归一化。
位置:全连接层中的仿射变换和激活函数之间。
全连接:
批量归一化:
(是batch_size大小,
代表batch中的每个样本,
是输出神经元维度的向量)
这⾥ϵ > 0是个很小的常数,保证分母大于0
引入可学习参数:拉伸参数和偏移参数
。引入目的:如果在模型中批量归一化得到的效果不好,那么可以通过学习到
和
的值来使批量归一化无效掉(因为若把这两个值带入到
中,可以得到
)。
表示按元素相乘。
对于batch的样本均值是0,标准差是1。
对卷积层做批量归一化
卷积层输出:batch_size×输出通道数×高×宽。
位置:卷积计算之后、应⽤激活函数之前。
如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数。
计算:对单通道,对该通道中 batch_size×高×宽 个元素同时做批量归一化,使用相同的均值和方差。
预测时的批量归一化
训练:以batch为单位,对每个batch计算均值和方差。
预测:由于预测的时候没有batch,因此采用移动平均法来估算整个训练数据集的样本均值和方差。
移动平均法
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean #momentum是一个超参数,前一项代表之前得到的均值,后一项代表现在更新的均值
moving_var = momentum * moving_var + (1.0 - momentum) * var
每次用新求得的均值mean和方差var来更新moving_mean和moving_var。momentum是一个超参数。
批量归一化的从零实现
def batch_norm(is_training, X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 判断当前模式是训练模式还是预测模式
if not is_training:
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps) #eps就是那个防止分母为0的小量
else:
assert len(X.shape) in (2, 4) #shape是2说明是全连层,是4说明是卷积层
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(dim=0) #在batch_size的维度上求平均
var = ((X - mean) ** 2).mean(dim=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持
# X的形状以便后面可以做广播运算
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 #momentum是一个超参数,前一项代表之前得到的均值,后一项代表现在更新的均值
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 拉伸和偏移
return Y, moving_mean, moving_var
class BatchNorm(nn.Module): #该类的作用:维护学习参数和超参数
def __init__(self, num_features, num_dims): #num_features:若全连层代表输出神经元个数;若卷积层代表通道数
super(BatchNorm, self).__init__()
if num_dims == 2:
shape = (1, num_features) #全连接层输出神经元
else:
shape = (1, num_features, 1, 1) #通道数
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape)) #由于 Y = gamma * X_hat + beta中乘是按元素乘(当然加也是按元素相加),而这里gamma和beta在其他几个维度上都是1,因此这里利用到了python的广播机制
# 不参与求梯度和迭代的变量,全在内存上初始化成0
self.moving_mean = torch.zeros(shape)
self.moving_var = torch.zeros(shape)
def forward(self, X):
# 如果X不在内存上,将moving_mean和moving_var复制到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)
# 保存更新过的moving_mean和moving_var, Module实例的traning属性默认为true, 调用.eval()后设成false
Y, self.moving_mean, self.moving_var = batch_norm(self.training,
X, self.gamma, self.beta, self.moving_mean,
self.moving_var, eps=1e-5, momentum=0.9)
return Y
上述从零实现应用到LeNet结构中
net = nn.Sequential(
nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
BatchNorm(6, num_dims=4),
nn.Sigmoid(),
nn.MaxPool2d(2, 2), # kernel_size, stride
nn.Conv2d(6, 16, 5),
BatchNorm(16, num_dims=4),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
d2l.FlattenLayer(),
nn.Linear(16*4*4, 120),
BatchNorm(120, num_dims=2),
nn.Sigmoid(),
nn.Linear(120, 84),
BatchNorm(84, num_dims=2),
nn.Sigmoid(),
nn.Linear(84, 10)
)
批量归一化的简洁实现(应用到LeNet结构中)
使用torch.nn中内置的批量归一化函数:
nn.BatchNorm1d(输出神经元个数) :表示全连层的批量归一化
nn.BatchNorm2d(输出通道数):表示卷积层的批量归一化
net = nn.Sequential(
nn.Conv2d(1, 6, 5), # in_channels, out_channels, kernel_size
nn.BatchNorm2d(6),
nn.Sigmoid(),
nn.MaxPool2d(2, 2), # kernel_size, stride
nn.Conv2d(6, 16, 5),
nn.BatchNorm2d(16),
nn.Sigmoid(),
nn.MaxPool2d(2, 2),
d2l.FlattenLayer(),
nn.Linear(16*4*4, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
d2l.train_ch5(net, train_iter, test_iter, batch_size, optimizer, device, num_epochs)
残差网络(ResNet)
深度学习的问题:深度CNN网络达到一定深度后再一味地增加层数并不能带来进一步地分类性能提高,反而会招致网络收敛变得更慢,准确率也变得更差。
残差网络可以有效缓解这一问题。
残差块(Residual Block)
ResNet中使用若干个由残差块组成的模块,每个模块由若干个(代码中的num_residuals)残差块组成。需要保证第一个模块的通道数同输入通道数一致。
每个模块中的第一个残差块用来:放缩通道数;调整卷积核步幅以减小特征图的高和宽。
如果在残差块中,输入通道数等于输出通道数,且特征图高和宽不变,那么输入X可以直接通过跨层线路与残差块的输出Y相加。否则,跨层线路中还需要使用额外的1×1卷积层来修改通道数或是修改卷积层的步幅,使输入X变成和残差块输出Y相同的形状。
在残差块中,输⼊可通过跨层的数据线路更快地向前传播。

举例说明残差块的作用:
若上图中左边:f(x)=x,则右边f(x)-x=0。易知右边的结构更易于捕捉恒等映射的细微波动。
残差块的实现
class Residual(nn.Module): # 本类已保存在d2lzh_pytorch包中方便以后使用
#可以设定输出通道数、是否使用额外的1x1卷积层来修改通道数以及卷积层的步幅。
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)
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: #可能需要使用额外的1x1卷积层来修改通道数以及卷积层的步幅。
X = self.conv3(X)
return F.relu(Y + X)
ResNet模型示例
模型结构
- 卷积层(64, 7×7, stride=2, padding=3)
- 批量归一化
- ReLU激活函数
- 最大池化(3×3, stride=2, padding=1)
- 残差块×4(通过步幅为2的残差块在每个模块之间减小高和宽)
- 全局平均池化
- 全连接
模型实现
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))
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)
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", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))
稠密连接网络(DenseNet)
主要构建模块:
- 稠密块(dense block):定义了输入和输出是如何连结的。
- 过渡层(transition layer):用来控制通道数,减小特征图高和宽,防止模型越来越复杂。
这里的“连结”:在通道维度上连结,稠密块的总通道数等于输入通道数加上稠密块中所有卷积层输出通道数的总和。

稠密块
- pytorch知识点:
class torch.nn.ModuleList(modules=None):可以添加子模块,将子模块保存在一个list中。可以像用list一样使用它,但不能直接把输入传给ModuleList。
稠密块的实现
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):
#num_convs:这里面用了几个conv_block,in_channels:整个DenseBlock的输入通道数。out_channels:每一个conv_block里的out_channels(它们全都相加再加上输入通道数才是这个DenseBlock的总输出通道数)
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) #nn.ModuleList():可以添加子模块,将子模块保存在一个list中。可以像用list一样使用它,但不能直接把输入传给ModuleList。
self.out_channels = in_channels + num_convs * out_channels # 计算输出通道数
def forward(self, X):
for blk in self.net: #像list一样使用self.net
Y = blk(X)
X = torch.cat((X, Y), dim=1) # 在通道维上将输入和输出连结
return X
过渡层
-
卷积层:来减小通道数
- 步幅为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模型
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_channels为当前的通道数,growth_rate是每经过一个卷积层会增长的通道数
num_convs_in_dense_blocks = [4, 4, 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", d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, num_channels, 1, 1)
net.add_module("fc", nn.Sequential(d2l.FlattenLayer(), nn.Linear(num_channels, 10)))
凸优化
优化与深度学习
- 本质上优化方法达到的目标与深度学习的目标并不相同:
- 优化方法目标:训练集损失函数值
- 深度学习目标:测试集损失函数值(泛化性)
- 优化在深度学习中的挑战:
- 局部最小值
- 鞍点
- 梯度消失
-
海森矩阵:是一个多元函数的二阶偏导数构成的方阵。
在一阶偏导数为0的点:若海森矩阵的特征值都为正数,则是局部极小值点;若都为负数,则是局部极大值点。若特征值有正有负,则为鞍点。
凸性(convexity)
对于一个集合内的任意两个点,如果这两个点连线上的所有点都在该集合内的话,就说该集合是凸集。
若有
,其中f的定义域是一个凸集,x和x'为f定义域上的任意两点,λ∈[0, 1]。则f是凸函数。
Jensen 不等式:
简单理解:函数值的期望大于等于期望的函数值。-
凸函数的性质:
- 无局部极小值
- 与凸集的关系:对于凸函数
,定义集合
,则集合
为凸集。
- 凸函数与二阶导数:
是凸函数
-
对于有限制条件的优化问题:
有以下几种解决方式:-
拉格朗日乘子法:
- 添加惩罚项:欲使
, 将项
加入目标函数,如多层感知机章节中的
- 投影:
投影法.png
-
拉格朗日乘子法:
梯度下降
一维梯度下降
泰勒展开:
代入沿梯度方向的移动量 :
更新式:
多维梯度下降
更新式:
自适应方法
牛顿法
在 处泰勒展开:
最小值点处满足: , 即我们希望
, 对上式关于
求导,忽略高阶无穷小,有:
更新式:
预处理 (Heissan阵辅助梯度下降)
梯度下降与线性搜索(共轭梯度法)
略
随机梯度下降
随机梯度下降参数更新
对于有 个样本对训练数据集,设
是第
个样本的损失函数, 则目标函数为:
其梯度为:
使用该梯度的一次更新的时间复杂度为
随机梯度下降更新公式 :
且有:
(每个样本的梯度是对整体梯度的无偏估计)
动态学习率
几种规划动态学习率的函数:
:对不同时间段的学习率赋值,一般
随着时间逐渐减小
小批量随机梯度下降
略
