目录
批量归一化和残差网络
批量归一化(BatchNormalization)
1.对全连接层做批量归一化
2.对卷积层做批量归⼀化
3.预测时的批量归⼀化
残差网络(ResNet)
残差块(Residual Block)
ResNet模型
稠密连接网络(DenseNet)
DenseNet模型
凸优化
梯度下降
随机梯度下降参数更新
动态学习率
小批量随机梯度下降
批量归一化和残差网络
批量归一化(BatchNormalization)
对输入的标准化(浅层模型)
处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。
标准化处理输入数据使各个特征的分布相近
批量归一化(深度模型)
利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定
1.对全连接层做批量归一化
位置:全连接层中的仿射变换和激活函数之间。
全连接:
x=Wu+boutput=ϕ(x) \boldsymbol{x} = \boldsymbol{W\boldsymbol{u} + \boldsymbol{b}} \output =\phi(\boldsymbol{x})
x=Wu+b
output=ϕ(x)
批量归一化: 对中间值x xx批量归一化为在batch_size上均值为0方差为1.
output=ϕ(BN(x)) output=\phi(\text{BN}(\boldsymbol{x}))
output=ϕ(BN(x))
y(i)=BN(x(i)) \boldsymbol{y}^{(i)} = \text{BN}(\boldsymbol{x}^{(i)})
μB←1m∑mi=1x(i), \boldsymbol{\mu}\mathcal{B} \leftarrow \frac{1}{m}\sum{i = 1}^{m} \boldsymbol{x}^{(i)},
σ2B←1m∑mi=1(x(i)−μB)2, \boldsymbol{\sigma}\mathcal{B}^2 \leftarrow \frac{1}{m} \sum{i=1}{m}(\boldsymbol{x}{(i)} - \boldsymbol{\mu}_\mathcal{B})^2,
σ
xˆ(i)←x(i)−μBσ2B+ϵ√, \hat{\boldsymbol{x}}^{(i)} \leftarrow \frac{\boldsymbol{x}^{(i)} - \boldsymbol{\mu}\mathcal{B}}{\sqrt{\boldsymbol{\sigma}\mathcal{B}^2 + \epsilon}},
x
这⾥ϵ > 0是个很小的常数,保证分母大于0
y(i)←γ⊙xˆ(i)+β. {\boldsymbol{y}}^{(i)} \leftarrow \boldsymbol{\gamma} \odot\hat{\boldsymbol{x}}^{(i)} + \boldsymbol{\beta}.
y
引入可学习参数:拉伸参数γ和偏移参数β。若γ=σ2B+ϵ−−−−−−√ \boldsymbol{\gamma} = \sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon}γ=
和β=μB \boldsymbol{\beta} = \boldsymbol{\mu}_\mathcal{B}β=μ
B
,批量归一化无效。
2.对卷积层做批量归⼀化
位置:卷积计算之后、应⽤激活函数之前。
如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数。
计算:对单通道,batchsize=m,卷积计算输出=pxq
对该通道中m×p×q个元素同时做批量归一化,使用相同的均值和方差。
3.预测时的批量归⼀化
训练:以batch为单位,对每个batch计算均值和方差。
预测:用移动平均估算整个训练数据集的样本均值和方差。
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)
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:
# 使用二维卷积层的情况,计算通道维上(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
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 拉伸和偏移
return Y, moving_mean, moving_var
以上定义了预测时的批量归一化,其中参数X是需要进行归一化的目标,参数momentum是超参数,用于更新移动平均的均值和方差。
class BatchNorm(nn.Module):
def init(self, num_features, num_dims):
super(BatchNorm, self).init()
if num_dims == 2:
shape = (1, num_features) #全连接层输出神经元
else:
shape = (1, num_features, 1, 1) #通道数
# 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
self.gamma = nn.Parameter(torch.ones(shape))
self.beta = nn.Parameter(torch.zeros(shape))
# 不参与求梯度和迭代的变量,全在内存上初始化成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
其中num_dims=2时代表全连接层的BN,此时num_features代表输出神经元的个数;num_dims=4时代表卷积层的BN,此时num_features代表输出的通道数。
实际应用时不需要自己动手写以上代码,在pytorch中有已经包装内置好的函数用来BN,直接调用即可。
nn.BatchNorm2d() 用于卷积层,括号内的参数为输入通道数;
nn.BatchNorm1d(120)用于全连接层,括号内的参数为神经元的个数。
应用于LeNet中:
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(1644, 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)
1
残差网络(ResNet)
深度学习的问题:深度CNN网络达到一定深度后再一味地增加层数并不能带来进一步地分类性能提高,反而会招致网络收敛变得更慢,准确率也变得更差。
残差块(Residual Block)
恒等映射:
左边(普通神经网络):f(x)=x
右边(引入残差块):f(x)-x=0 (易于捕捉恒等映射的细微波动)
在残差块中,输⼊可通过跨层的数据线路更快地向前传播。
ResNet模型
卷积(64,7x7,3)
批量一体化
最大池化(3x3,2)
残差块x4 (通过步幅为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))
dd_module(“global_avg_pool”, d2l.GlobalAvgPool2d()) # GlobalAvgPool2d的输出: (Batch, 512, 1, 1)
net.add_module(“fc”, nn.Sequential(d2l.FlattenLayer(), nn.Linear(512, 10)))
1
2
稠密连接网络(DenseNet)
主要构建模块:
稠密块(dense block): 定义了输入和输出是如何连结的。
过渡层(transition layer):用来控制通道数,使之不过大。
稠密块
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
1
2
稠密块的使用会使得通道数越来越多,这里使用过渡层来减小模型的复杂度。
过渡层
1×1 1\times11×1卷积层:来减小通道数
步幅为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
blk = transition_block(23, 10)
blk(Y).shape # torch.Size([4, 10, 4, 4])
1
2
3
4
5
6
7
8
9
10
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))
1
2
3
4
5
之后接入(稠密块—过渡层)3
这里稠密块为4层卷积,过渡层为减半。最开始稠密块输入通道数为64,每一个卷积层输出新增通道数32。因此通道数变化为:64 — 64+432=192 — 192/2=96 — 96+4*32=224 — 224/2…以此类推下去。
num_channels, growth_rate = 64, 32 # num_channels为当前的通道数
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
1
2
3
4
5
6
7
8
9
10
11
12
最后接上剩下的结构:
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)))
X = torch.rand((1, 1, 96, 96))
for name, layer in net.named_children():
X = layer(X)
print(name, ’ output shape:\t’, X.shape)
1
2
3
4
5
6
7
8
9
另外1*1卷积层使用地方很多,它可以保持高宽不变有效的降低数据维度如下图:
凸优化
尽管优化方法可以最小化深度学习中的损失函数值,但本质上优化方法达到的目标与深度学习的目标并不相同。
优化方法目标:训练集损失函数值
深度学习目标:测试集损失函数值(泛化性)
鞍点是对所有自变量一阶偏导数都为0,且Hessian矩阵特征值有正有负的点。
梯度下降
梯度下降、随机梯度下降和小批量随机梯度下降三者的区别在于每次更新时用的样本量。
对于梯度下降,每次更新参数时使用的是整个数据集,选择所有样本的平均梯度来对参数进行更新,这种更新方法对于数据会有一定的相似性,对于共性比较大的数据集这种方法对于数据的利用率比较低。
对于随机梯度下降,每次更新参数时使用的是单个样本点的梯度,这种方法算力的利用率比较低,并没有充分利用上目前CPU或GPU的矢量运算能力。
小批量随机梯度下降可以中和以上两种方法,小批量随机梯度下降每次在进行参数更新的时候选择的是一部分的样本,对这些样本的梯度取均值对参数进行更新。
随机梯度下降参数更新
对于有 n nn 个样本对训练数据集,设 fi(x) f_i(x)f
i
(x) 是第 i ii 个样本的损失函数, 则目标函数为:
f(x)=1n∑ni=1fi(x) f(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} f_{i}(\mathbf{x})
其梯度为:
∇f(x)=1n∑ni=1∇fi(x) \nabla f(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} \nabla f_{i}(\mathbf{x})
∇f(x)=
n
使用该梯度的一次更新的时间复杂度为 O(n) \mathcal{O}(n)O(n),即梯度下降的时间复杂度是 O(n) \mathcal{O}(n)O(n)。
随机梯度下降更新公式 O(1) \mathcal{O}(1)O(1):
x←x−η∇fi(x) \mathbf{x} \leftarrow \mathbf{x}-\eta \nabla f_{i}(\mathbf{x})
x←x−η∇f
i
(x)
随机梯度下降的时间复杂度是 O(1) \mathcal{O}(1)O(1)。
且有:
Ei∇fi(x)=1n∑ni=1∇fi(x)=∇f(x) \mathbb{E}{i} \nabla f{i}(\mathbf{x})=\frac{1}{n} \sum_{i=1}^{n} \nabla f_{i}(\mathbf{x})=\nabla f(\mathbf{x})
E
i
(x)=∇f(x)
对于随机梯度下降,每次更新参数时使用的是单个样本点的梯度,这种方法算力的利用率比较低,并没有充分利用上目前CPU或GPU的矢量运算能力。
动态学习率
η(t)=ηi if ti≤t≤ti+1η(t)=η0⋅e−λtη(t)=η0⋅(βt+1)−α piecewise constant exponential polynomial \begin{array}{ll}{\eta(t)=\eta_{i} \text { if } t_{i} \leq t \leq t_{i+1}} & {\text { piecewise constant }} \ {\eta(t)=\eta_{0} \cdot e^{-\lambda t}} & {\text { exponential }} \ {\eta(t)=\eta_{0} \cdot(\beta t+1)^{-\alpha}} & {\text { polynomial }}\end{array}
η(t)=η
i
if t
i
piecewise constant
exponential
polynomial
动态学习率在最开始学习率设计比较大,加速收敛;随着迭代次数增加减小学习率,学习率可以设计为指数衰减或多项式衰减;在优化进行一段时间后可以适当减小学习率来避免振荡。
小批量随机梯度下降
小批量随机梯度下降可以中和以上两种方法。
小批量随机梯度下降每次在进行参数更新的时候选择的是一部分的样本,对这些样本的梯度取均值对参数进行更新。
读取数据
def get_data_ch7(): # 本函数已保存在d2lzh_pytorch包中方便以后使用
data = np.genfromtxt(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’)
data = (data - data.mean(axis=0)) / data.std(axis=0) # 标准化
return torch.tensor(data[:1500, :-1], dtype=torch.float32),
torch.tensor(data[:1500, -1], dtype=torch.float32) # 前1500个样本(每个样本5个特征)
features, labels = get_data_ch7()
features.shape
1
其中np.genfromtxt()函数是用来创建数组表格数据,详情见:genfromtxt函数解析
import pandas as pd
df = pd.read_csv(’/home/kesci/input/airfoil4755/airfoil_self_noise.dat’, delimiter=’\t’, header=None)
df.head(10)
1
0-4列为特征,5列为标签。