4.PyTorch损失优化
4.1.权值初始化
4.1.1.梯度消失与爆炸
对于一个含有多层隐藏层的神经网络来说,当梯度消失发生时,接近于输出层的隐藏层由于其梯度相对正常,所以权值更新时也就相对正常,但是当越靠近输入层时,由于梯度消失现象,会导致靠近输入层的隐藏层权值更新缓慢或者更新停滞。这就导致在训练时,只等价于后面几层的浅层网络的学习。梯度爆炸与之相反。
例如下图的神经网络:
其中,,,
对求导得:
从上式可以看出,损失函数对求导是由多个求导累乘的结果,对于其中的每个求导,如果此部分小于1,那么随着层数增多,求出的梯度更新信息将会以指数形式衰减,即发生了梯度消失,如果此部分大于1,那么层数增多的时候,最终的求出的梯度更新将以指数形式增加,即发生梯度爆炸。
代码实现:
# -*- coding: utf-8 -*-
import torch
import random
import numpy as np
import torch.nn as nn
def set_seed(seed=1):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
set_seed(3) # 设置随机种子
class MLP(nn.Module):
def __init__(self, neural_num, layers):
super(MLP, self).__init__()
# 构建layers层,每层neural_num个神经元的神经网络
self.linears = nn.ModuleList([nn.Linear(neural_num, neural_num, bias=False) for i in range(layers)])
self.neural_num = neural_num
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
return x
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# 参数初始化,对每层的权重初始化为均值为0,标准差为1的正太分布。
nn.init.normal_(m.weight.data)
# 层数
layer_nums = 100
# 每层神经元个数
neural_nums = 256
# 批大小
batch_size = 16
# 构建MLP模型
net = MLP(neural_nums, layer_nums)
net.initialize()
inputs = torch.randn((batch_size, neural_nums)) # normal: mean=0, std=1
output = net(inputs)
print(output)
tensor([[nan, nan, nan, ..., nan, nan, nan], [nan, nan, nan, ..., nan, nan, nan], ..., [nan, nan, nan, ..., nan, nan, nan], [nan, nan, nan, ..., nan, nan, nan]], grad_fn=<ReluBackward0>) |
可以看到最终输出的全部为nan,可以打印每层的标准差,查看每层的输出情况,以及最终出现nan层的标准差,在MLP类forward方法中,遍历时进行输出
代码实现:
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
# 打印每层标准差
print("layer:{}, std:{}".format(i, x.std()))
if torch.isnan(x.std()):
# 判断如果当前层的标准差已经是nan,则打印当前层并跳出循环
print("output is nan in {} layers".format(i))
break
return x
layer:0, std:15.959932327270508 layer:1, std:256.6237487792969 ... layer:29, std:1.322983152787379e+36 layer:30, std:2.0786820453988485e+37 layer:31, std:nan output is nan in 31 layers tensor([[ inf, -2.6817e+38, inf, ..., inf, inf, inf], [ -inf, -inf, 1.4387e+38, ..., -1.3409e+38, -1.9659e+38, -inf], ..., [ inf, inf, -inf, ..., -inf, inf, 1.7432e+38]], grad_fn=<MmBackward>) |
可以看到上面在第31层时,输出以及很大或者很小,为何会出现这样的问题?
首先,对于方差有
若E(X)=0 ,则
又对于第一层隐藏层的第一个元素:
则这个元素对应的方差为:
标准差则为
可以看到,每向后传播一层,则标准差扩大倍,因此,当层数很多时会变为无穷大。
为了解决上述问题,可以设置让每层传播时的方差变为1,即
得:
所以,对于上面的代码,可以改造为每层的权重为均值0,标准差,这样就可以避免每层标准差成倍增长的问题。
代码实现:
修改MLP类initialize中初始化权重的方法
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight.data, std=np.sqrt(1/self.neural_num))
layer:0, std:0.9974957704544067 layer:1, std:1.0024365186691284 ... layer:98, std:1.1617802381515503 layer:99, std:1.2215303182601929 tensor([[-1.0696, -1.1373, 0.5047, ..., -0.4766, 1.5904, -0.1076], [ 0.4572, 1.6211, 1.9659, ..., -0.3558, -1.1235, 0.0979], ..., [-0.5871, -1.3739, -2.9027, ..., 1.6734, 0.5094, -0.9986]], grad_fn=<MmBackward>) |
4.1.2.常用初始化方法
对于之前手动进行权值初始化,PyTorch提供了一些常用的权值初始化方法
Xavier初始化
方差一致性:保持数据尺度维持在恰当范围,通常方差为1
激活函数:饱和函数,如Sigmoid, Tanh
方差的计算法则为:
,
其中,表示输入层的神经元数量,表示输出层的神经元数量。
代码实现:
在之前的代码基础上,首先在MLP类forward方法中,为每层添加sigmoid激活函数
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
# 添加sigmoid激活函数
x = torch.sigmoid(x)
然后,修改MLP类initialize中初始化权重的方法为Xavier初始化
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# 权重初始化使用xavier均匀分布进行初始化
nn.init.xavier_uniform_(m.weight.data)
layer:0, std:0.20717598497867584 layer:1, std:0.1237645372748375 ... layer:98, std:0.12034578621387482 layer:99, std:0.11722493171691895 tensor([[0.5740, 0.5291, 0.8039, ..., 0.4145, 0.3551, 0.7414], [0.5740, 0.5291, 0.8039, ..., 0.4145, 0.3551, 0.7414], ..., [0.5740, 0.5291, 0.8039, ..., 0.4145, 0.3551, 0.7414]], grad_fn=<SigmoidBackward>) |
可以看到标准差始终控制在0.12左右
Kaiming初始化
差一致性:保持数据尺度维持在恰当范围,通常方差为1
激活函数: ReLU及其变种
方差的计算法则为:
对于ReLU变种:
其中,表示输入层的神经元数量,a为激活函数在负半轴的斜率
代码实现:
在之前的代码基础上,首先在MLP类forward方法中,每层激活函数改为relu
def forward(self, x):
for (i, linear) in enumerate(self.linears):
x = linear(x)
# 使用relu激活函数
x = torch.relu(x)
然后,修改MLP类initialize中初始化权重的方法为Kaiming初始化
def initialize(self):
for m in self.modules():
if isinstance(m, nn.Linear):
# 权重初始化使用kaiming正太分布进行初始化
nn.init.kaiming_normal_(m.weight.data)
layer:0, std:0.826629638671875 layer:1, std:0.8786815404891968 ... layer:98, std:0.6579315066337585 layer:99, std:0.6668476462364197 tensor([[0.0000, 1.3437, 0.0000, ..., 0.0000, 0.6444, 1.1867], [0.0000, 0.9757, 0.0000, ..., 0.0000, 0.4645, 0.8594], ..., [0.0000, 1.1807, 0.0000, ..., 0.0000, 0.5668, 1.0600]], grad_fn=<ReluBackward0>) |
可以看到标准差始终控制在0.5-1左右
十种初始化方法
1. Xavie r均匀分布
2. Xavie r正态分布
3. Kaiming均匀分布
4. Kaiming正态分布
5. 均匀分布
6. 正态分布
7. 常数分布
8. 正交矩阵初始化
9. 单位矩阵初始化
10. 稀疏矩阵初始化
4.2.损失函数
4.2.1.损失函数概述
损失函数:衡量模型输出与真实标签的差异
常见的概念:
损失函数(Loss Function): ,即一个样本预测值与真实值差异
代价函数(Cost Function): ,即所有样本的差异的平均
目标函数(Objective Function): Regularization,即代价函数+正则项
通常将损失函数和代价函数统称为损失函数,使用代价函数的计算结果。
4.2.2.交叉熵损失函数
交叉熵 = 信息熵 + 相对熵
熵:亦称为信息熵,用来描述事件的不确定性,事件越不确定,熵越大
熵的计算公式:
自信息:,表示单个事件的不确定性
相对熵:用来衡量两个分布之间的差异
计算公式:
其中表示真实的分布,表示模型输出的分布
交叉熵:
对相对熵公式展开得: