1. 数据标准化处理
输入层使用,使输入数据各个特征的分布相近
为什么要这样做?
- 神经网络学习的本质就是学习数据的分布,如果训练数据与测试数据的分布不同,那么网络的泛化能力也大大降低;
- 在使用小批量数据对神经网络进行训练时,若每批训练数据的分布各不相同,网络在每次迭代都去学习适应不同的分布,这会大大降低网络的训练速度;
2.为什么要使用批量归一化?
使用浅层模型时,随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化,对深层神经网络来说,随着网络训练的进行,前一层参数的调整使得后一层输入数据的分布发生变化,各层在训练的过程中就需要不断的改变以适应学习这种新的数据分布。所以即使输入数据已做标准化,训练中模型参数的更新依然很容易导致后面层输入数据分布的变化,只要网络的前面几层发生微小的改变,那么后面几层就会被累积放大下去。最终造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。如果训练过程中,训练数据的分布一直在发生变化,那么将不仅会增大训练的复杂度,影响网络的训练速度而且增加了过拟合的风险。
批量归一化可以看作在每一层输入和上一层输出之间加入了一个新的计算层,对数据的分布进行额外的约束,从而增强模型的泛化能力。但是批量归一化同时也降低了模型的拟合能力,归一化之后的输入分布被强制拉到均值为0和标准差为1的正态分布上来。以Sigmoid激活函数为例,批量归一化之后数据整体处于函数的非饱和区域,只包含线性变换(多层的线性函数跟一层线性网络是等价的,网络的表达能力下降),破坏了之前学习到的特征分布。为了恢复原始数据分布,保证非线性的获得,引入了变换重构以及可学习参数γ和β,其对应于输入数据分布的方差和偏差
3.批量归一化的优势:
- 不加批量归一化的网络需要慢慢的调整学习率时,网络中加入批量归一化时,可以采用初始化很大的学习率,然后学习率衰减速度也很大,因此这个算法收敛很快。
- 大大提高模型训练速度,提高网络泛化性能。
- 数据批量归一化后相当于只使用了S型激活函数的线性部分,可以缓解S型激活函数反向传播中的梯度消失的问题。
- 深层神经网络在做非线性变换前的激活输入值,随着网络深度加深或者在训练过程中,其分布逐渐发生偏移或者变动,整体分布逐渐往非线性函数的取值区间的上下限两端靠近(下图左图),这会导致反向传播时低层神经网络的梯度消失,BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0方差为1的标准正态分布(如右图),使得激活输入值落在非线性函数对输入比较敏感的线性区域,其对应的导数远离导数饱和区 ,这样输入的小变化就会导致损失函数较大的变化,避免梯度消失问题产生,学习收敛速度快,能大大加快训练速度。
4.如何使用批量归一化?
- 求每一个训练批次数据的均值
- 求每一个训练批次数据的方差
- 使用求得的均值和方差对该批次的训练数据做归一化,获得0-1分布。其中ε是为了避免除数为0时所使用的微小正数。
- 尺度变换和偏移:将xi乘以γ调整数值大小,再加上β增加偏移后得到yi,这里的γ是尺度因子,β是平移因子。这一步是BN的精髓,由于归一化后的xi基本会被限制在正态分布下,使得网络的表达能力下降。为解决该问题,我们引入两个新的参数:γ,β。 γ和β是在训练时网络自己学习得到的。
a中左图是没有经过任何处理的输入数据,曲线是sigmoid函数,如果数据在梯度很小的区域,那么学习率就会很慢甚至陷入长时间的停滞。减均值除方差后,数据就被移到中心区域如右图所示,对于大多数激活函数而言,这个区域的梯度都是最大的或者是有梯度的(比如ReLU),这可以看做是一种对抗梯度消失的有效手段。对于一层如此,如果对于每一层数据都那么做的话,数据的分布总是在随着变化敏感的区域,相当于不用考虑数据分布变化了,这样训练起来更有效率。
那么为什么要有第4步,不是仅使用减均值除方差操作就能获得目的效果吗?
因为减均值除方差操作后可能会削弱网络的性能!针对该情况,在前面三步之后加入第4步完成真正的batch normalization
BN的本质就是利用优化变一下方差大小和均值位置,使得新的分布更切合数据的真实分布,保证模型的非线性表达能力。BN的极端的情况就是这两个参数等于mini-batch的均值和方差,那么经过batch normalization之后的数据和输入完全一样,当然一般的情况是不同的
4.1 全连接层:
将批量归一化层置于全连接层中的仿射变换和激活函数之间
4.2 卷积层:
在卷积计算之后、应用激活函数之前
卷积层上的 BN 使用,其实也是使用了类似权值共享的策略,把一整张特征图当做一个神经元进行处理。卷积神经网络经过卷积后得到的是一系列的特征图,如果 min-batch sizes 为 m,那么网络某一层输入数据可以表示为四维矩阵(m,f,w,h),m 为 min-batch sizes,f 为特征图个数,w、h 分别为特征图的宽高。在 CNN 中我们可以把每个特征图看成是一个特征处理(一个神经元),因此在使用 Batch Normalization,mini-batch size 的大小就是:m*w*h,于是对于每个特征图都只有一对可学习参数:γ、β。说白了就是相当于求取所有样本所对应的一个特征图的所有神经元的平均值、方差,然后对这个特征图神经元做归一化。
BN在深层神经网络的作用非常明显:若神经网络训练时遇到收敛速度较慢,或者“梯度爆炸”等无法训练的情况发生时都可以尝试用BN来解决。同时,常规使用情况下同样可以加入BN来加速模型训练,甚至提升模型精度
4.3 预测时均值和方差怎么求?
训练时,对同一批的数据的均值和方差进行求解,进而进行归一化操作
以下是一种可能的方案
在模型训练时我们就记录下每个batch下的均值和方差,待训练完毕后,我们求整个训练样本的均值和方差期望值,作为我们进行预测时进行BN的的均值和方差
5.代码实现
import torch
from torch import nn, optim
import torchvision
import sys
from time import time
device = torch.device('cuda:1' if torch.cuda.is_available() else 'cpu')
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:
'''训练模式'''
# 判断输入数据 X 是全连接层还是卷积层
assert len(X.shape) in (2, 4)
''' 全连接层'''
if len(X.shape) == 2:
# 沿纵向求均值,(1, 特征个数)
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)
# 以上代码可以优化为
# mean = X.mean(dim=(0, 2, 3), keepdim=True)
var = ((X - mean) ** 2).mean(dim=0, keepdim=True).mean(dim=2, keepdim=True).mean(dim=3, keepdim=True)
# 以上代码可以优化为
# var = ((X - mean) ** 2)(dim=(0, 2, 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
用于批量归一化操作的函数 batch_norm 的输入参数,具体含义如下:
is_training:一个布尔值,表示当前是否处于训练模式。在训练过程中,会计算并更新移动平均值和移动方差,而在推理(inference)过程中,直接使用已经计算好的移动平均值和移动方差进行归一化操作。
X:输入数据,可以是全连接层的输出或卷积层的输出。
gamma:尺度参数,可学习参数,用于对归一化后的数据进行尺度变换。它的形状与 X 的形状相同。
beta:偏移参数,可学习参数,用于对归一化后的数据进行偏移操作。它的形状与 X 的形状相同。
moving_mean:移动平均值,记录训练过程中的平均值。在训练过程中,会通过指数平滑算法不断更新。它的形状与 X 的形状相同。
moving_var:移动方差,记录训练过程中的方差。在训练过程中,会通过指数平滑算法不断更新。它的形状与 X 的形状相同。
eps:一个小的常数,用于避免除零错误。当计算标准差时,如果方差过小,会出现除以接近零的情况,为了稳定计算,引入 eps 来避免这个问题。
momentum:一个介于 0 和 1 之间的参数,表示移动平均值和移动方差在指数平滑算法中的衰减率。较大的 momentum 值可以使模型更加稳定,但可能导致较长的收敛时间。
使用一阶指数平滑算法的批量归一化
一阶指数平滑算法(First-order Exponential Smoothing)是一种用于计算移动平均值的方法,常用于时间序列数据的平滑处理。它的基本思想是对当前的观测值进行加权平均,同时考虑过去观测值的影响。
在批量归一化中,一阶指数平滑算法用于更新移动平均值和移动方差。具体步骤如下:
1.假设当前的观测值为 x,移动平均值为 m_old。
2.计算新的移动平均值 m_new,通过以下公式计算: m_new = momentum * m_old + (1 - momentum) * x 其中momentum 是一个介于 0 和 1 之间的参数,表示过去观测值在计算中所占的权重,通常取较小的值,如 0.9。
3.更新移动平均值 m_old 为 m_new,用于下一次的计算。
通过一阶指数平滑算法,移动平均值可以不断地根据当前观测值进行调整,使其更好地反映数据的趋势变化。在批量归一化中,使用一阶指数平滑算法更新移动均值和移动方差可以提高模型的稳定性和收敛速度。
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)
# 调用 batch_norm 函数进行批量归一化操作
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
class LeNet_1(nn.Module):
def __init__(self):
super(LeNet_1, self).__init__()
self.conv = nn.Sequential(
#c1
nn.Conv2d(1, 6, 5),
nn.BatchNorm2d(6),
nn.Sigmoid(),
#s2
nn.MaxPool2d(2, 2),
#c3
nn.Conv2d(6, 16, 5),
nn.BatchNorm2d(16),
nn.Sigmoid(),
#s4
nn.MaxPool2d(2, 2)
)
self.fc = nn.Sequential(
nn.Linear(16*4*4, 120),
nn.BatchNorm1d(120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.BatchNorm1d(84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
def forward(self, img):
feature = self.conv(img)
output = self.fc(feature.view(img.shape[0], -1))
return output
net = LeNet_1()
print(net)
训练
'''定义读取数据集函数'''
def load_data_fashion_mnist(batch_size, resize=None):
if sys.platform.startswith('win'):
num_workers = 0 # 0表示不用额外的进程来加速读取数据
else:
num_workers = 4
'''定义数据预处理的转换函数列表'''
trans = []
if resize: #判断是否需要进行图像尺寸调整(resize)
trans.append(torchvision.transforms.Resize(size=resize))
#将torchvision.transforms.Resize转换函数添加到转换函数列表trans中,并指定目标尺寸为resize
trans.append(torchvision.transforms.ToTensor())
# 将torchvision.transforms.ToTensor转换函数添加到转换函数列表trans中。这个函数用于将图像数据转换为张量,并且按照通道顺序排列(RGB)
transform = torchvision.transforms.Compose(trans)
#通过torchvision.transforms.Compose函数将转换函数列表trans组合成一个转换操作
mnist_train = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=True,
download=True,
transform=transform)
mnist_test = torchvision.datasets.FashionMNIST(root='data/FashionMNIST',
train=False,
download=True,
transform=transform)
train_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=True,
num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_train,
batch_size=batch_size,
shuffle=False,
num_workers=num_workers)
return train_iter, test_iter
'''用于GPU的准确率评估函数'''
def evaluate_accuracy(data_iter, net, device=None):
if device is None and isinstance(net, torch.nn.Module):
device = list(net.parameters())[0].device #如果没指定device就使用net的device
acc_sum, n = 0.0, 0
for X, y in data_iter:
if isinstance(net, torch.nn.Module):
net.eval() # 评估模式, 这会关闭dropout
acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
net.train() # 改回训练模式
else:
if ('is_training' in net.__code__.co_varnames):
acc_sum += (net(X, is_training=False).argmax(dim=1) == y).float().sum().item()
else:
acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
n += y.shape[0]
return acc_sum / n
'''定义测试函数,确保计算使用的数据和模型同在内存或显存上'''
def train_ch5(net, train_iter, test_iter, batch_size, optimizer , device, num_epochs):
# 将模型加载到指定运算器中
net = net.to(device)
print("training on ", device)
loss = torch.nn.CrossEntropyLoss()
for epoch in range(num_epochs):
# 在每一个迭代周期中,会使用训练集中所有样本一次(假设样本数能够被批量大小整除)
train_l_sum, train_acc_sum, n ,batch_count= 0.0, 0.0, 0,0
# 分别表示训练损失总和、训练准确度总和、样本数
start = time()
for X, y in train_iter:
X = X.to(device)
y = y.to(device)
'''前向传播'''
y_hat = net(X)
#由于变量 l 并不是一个标量,所以我们可以调用 .sum() 将其求和得到一个标量
'''计算损失'''
l = loss(y_hat, y).sum()
'''梯度清零'''
if optimizer is not None:
optimizer.zero_grad()
elif params is not None and params[0].grad is not None:
for param in params:
param.grad.data.zero_()
'''反向传播'''
l.backward() # 运行 l.backward() 得到该变量有关模型参数的梯度
if optimizer is None:
d2l.sgd(params, lr, batch_size)
else:
'''更新参数'''
optimizer.step()
'''计算精度'''
train_l_sum += l.cpu().item()
train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
n += y.shape[0]
batch_count += 1
test_acc = evaluate_accuracy(test_iter, net)
print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f,\
time %.1f sec' %(epoch+1, train_l_sum/batch_count,
train_acc_sum/n, test_acc,
time()-start))
'''训练模型'''
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
lr, num_epochs = 0.001, 5
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
train_ch5(net, train_iter, test_iter, batch_size,optimizer, device, num_epochs)