这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战
一 简介
和Sigmoid、tanh激活函数不同,ReLU激活函数的叠加并不会出现梯度消失或者梯度爆炸,但ReLU激活函数中使得部分数值归零的特性却会导致另外一个严重的问——Dead ReLU Problem,也被称为神经元活性失效问题。
二 Dead ReLU Problem 实验
首先通过实验来观察神经元活性失效问题(Dead ReLU Problem),在建模过程中的直接表现,ReLU叠加模型在迭代多次后在MSE取值高位收敛的情况,其实就是出现了神经元活性失效所导致的问题。
import random
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
import numpy as np
import torch
from torch import nn,optim
import torch.nn.functional as F
from torch.utils .data import Dataset,TensorDataset,DataLoader
from torch.utils.data import random_split
from torch.utils.tensorboard import SummaryWriter
复制代码
本文涉及到的自建函数
#回归类数据集创建函数
def tensorGenReg(num_examples=1000,w=[2,-1,1],bias=True,delta=0.01,deg=1):
"""
回归类数据集创建函数
param num_examples:创建数据集的张量
param w:包括截距的(如果存在)特征系数张量
param bias :是否需要截距
param delta:扰动项取值
param deg :方程次数
return:生成的特征张量和标签张量
"""
if bias==True:
num_inputs=len(w)-1 #特征张量
features_true=torch.randn(num_examples,num_inputs) #不包括全是1 的列的特征张量
w_true=torch.tensor(w[:1].reshape(-1,1).float()) #自变量系数
b_true=torch.tensor(w[-1]).float()
if num_inputs==1: #若输入的特征只有1个,则不能使用矩阵乘法
labels_true=torch.pow(features_ture,deg)*w_true+b_true
else:
labels_true=torch.mm(torch.pow(features_true,deg),w_true)+b_true
features=torch.cat((features_true,torch.ones(len(features_true),1)),1) #在张量的最后添加一列全是1的列
labels=labels_true+torch.randn(size=labels_true.shape)*delta
else:
num_inputs=len(w)
features=torch.randn(num_examples,num_inputs)
w_true=torch.tensor(w).reshape(-1,1).float()
if num_inputs==1:
labels_true=torch.pow(features,deg)*w_true
else:
labels_true=torch.mm(torch.pow(features,deg),w_true)
labels=labels_true+torch.randn(size=labels_true.shape)*delta
return features,labels
#适用于封装自定义数据集的类
class GenData(Dataset):
def __init__(self,features,labels):
self.features=features
self.labels=labels
self.lens=len(features)
def __getitem__(self,index):
return self.features[index,:], self.labels[index]
def __len__(self):
return self.lens
def fit(net,criterion,optimizer,batchdata,epochs=3,cla=False):
"""
模型训练函数
parma net:待训练的模型
param criterion:损失函数
param optimizer:优化算法
param batchdata:训练数据集
param cla :是否是分类问题
param epochs:遍历数据次数
"""
for epoch in range(epochs):
for X,y in batchdata:
if cla ==True:
y=y.flatten().long() #如果是分类问题,需要对y进行整数转化
yhat=net.forward(X)
loss=criterion(yhat,y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
def split_loader(features,labels,batch_size=10,rate=0.7):
"""
数据封装,切分和加载函数
param features:输入的特征
param labels:数据集标签张量
param batch_size:数据加载时的每一个小批量数据
param rate:训练数据集占比
return:加载好的训练集和测试集
"""
data=GenData(features,labels)
num_train=int(data.lens*0.7)
num_test=data.lens-num_train
data_train,data_test=random_split(data,[num_train,num_test])
train_loader=DataLoader(data_train,batch_size=batch_size,shuffle=True)
test_loader=DataLoader(data_test,batch_size=batch_size,shuffle=True)
return (train_loader,test_loader)
class ReLU_class3(nn.Module):
def __init__(self,in_features=2,n_hidden1=4,n_hidden2=4,n_hidden3=4,out_features=1,bias=True,BN_model=None):
super(ReLU_class3,self).__init__()
self.linear1=nn.Linear(in_features,n_hidden1,bias=bias)
self.normalize1=nn.BatchNorm1d(n_hidden1)
self.linear2=nn.Linear(n_hidden1,n_hidden2,bias=bias)
self.normalize2=nn.BatchNorm1d(n_hidden2)
self.linear3=nn.Linear(n_hidden2,n_hidden3,bias=bias)
self.normalize3=nn.BatchNorm1d(n_hidden3)
self.linear4=nn.Linear(n_hidden3,out_features,bias=bias)
self.BN_model=BN_model
def forward(self, x):
if self.BN_model == None:
z1 = self.linear1(x)
p1 = torch.relu(z1)
z2 = self.linear2(p1)
p2 = torch.relu(z2)
z3 = self.linear3(p2)
p3 = torch.relu(z3)
out = self.linear4(p3)
elif self.BN_model == 'pre':
z1 = self.normalize1(self.linear1(x))
p1 = torch.relu(z1)
z2 = self.normalize2(self.linear2(p1))
p2 = torch.relu(z2)
z3 = self.normalize3(self.linear3(p2))
p3 = torch.relu(z3)
out = self.linear4(p3)
elif self.BN_model == 'post':
z1 = self.linear1(x)
p1 = torch.relu(z1)
z2 = self.linear2(self.normalize1(p1))
p2 = torch.relu(z2)
z3 = self.linear3(self.normalize2(p2))
p3 = torch.relu(z3)
out = self.linear4(self.normalize3(p3))
return out
def mse_cal (data_loader,net):
"""
mse计算函数
param data_loader:加载好的数据
param net:模型
return:根据数据的数据,输出其mse计算结果
"""
data=data_loader.dataset #还原Dataset类
X=data[:][0]
y=data[:][1]
yhat=net(X)
return F.mse_loss(yhat,y)
def model_train_test(model
,train_data
,test_data
,num_epochs=20
,criterion=nn.MSELoss()
,optimizer=optim.SGD
,lr=0.03
,cla=False
,eva=mse_cal
):
"""
模型误差测试数据
param model_l:模型
param train_data:训练数据
param test_data:测试数据
param num_epochs:迭代轮数
param criterion:损失函数
param lr:学习率
param cla:是否是分类模型
return:MSE 列表
"""
#模型评估指标
train_l=[]
test_l=[]
#模型训练
for epochs in range(num_epochs):
model.train()
fit(net=model
,criterion=criterion
,optimizer=optimizer(model.parameters(),lr=lr)
,batchdata=train_data
,epochs=epochs
,cla=cla
)
model.eval()
train_l.append(eva(train_data,model).detach())
test_l.append(eva(test_data,model).detach())
return train_l,test_l
复制代码
#设置随机种子
torch.manual_seed(420)
#创建最高项为2的多项式回归数据集
features,labels=tensorGenReg(w=[2,-1],bias=False,deg=2)
#进行数据集切分与加载
train_loader,test_loader=split_loader(features,labels)
复制代码
#创建随机数种子
torch.manual_seed(420)
#实例化模型
relu_model3=ReLU_class3(bias=False) #创建布袋截距项的模型
#核心参数
num_epochs=20
lr=0.03
#模型训练
train_l,test_l=model_train_test(relu_model3
,train_loader
,test_loader
,num_epochs=num_epochs
,criterion=nn.MSELoss()
,optimizer=optim.SGD
,lr=0.03
,cla=False
,eva=mse_cal
)
#绘制图像,查看mse变化情况
plt.plot(list(range(num_epochs)),train_l,label='train_mse')
plt.plot(list(range(num_epochs)),test_l,label='test_mse')
plt.legend(loc=4)
复制代码
模型在迭代多轮之后,训练误差和测试误差都在各自取值的高位收敛了,也就是误差不随模型迭代测试增加而递减。通过简单尝试我们不难发现,此时模型对所有数据的输出结果都是0。
relu_model3(features)
复制代码
三 Deed ReLU Problem成因分析
神经元活性失效问题和ReLU激活函数本身特性有关。首先,观察ReLU激活函数函数图像与导函数图像。
#绘制relu函数的函数图像和导函数图像
X=torch.arange(-5,5,0.1)
X.requires_grad=True
relu_y=torch.relu(X)
#反向传播
relu_y.sum().backward()
#relu函数图像
plt.subplot(121)
plt.plot(X.detach(),relu_y.detach())
plt.title('ReLU Function')
#ReLU导函数图像
plt.subplot(122)
plt.plot(X.detach(),X.grad.detach())
plt.title('ReLU Derivative Function')
复制代码
- 对于ReLU激活函数来说,只要激活函数接收到的数据小于0,输出结果就全是0,并且更关键的是,只要ReLU输出结果是0,由于ReLU的导函数是分段常数函数且接收数据为负时导数为0,因此如果ReLU输出结果为零,则反向传播结果、也就是各层的梯度,也都是零。
- 当某条数据在模型中的输出结果为0时,反向传播后各层参数的梯度也全为0,此时参数将无法通过迭代更新。而进一步的,如果在某种参数情况下,整个训练数据集输入模型之后输出结果都是0,则在小批量梯度下降的情况下,每次再挑选出一些数据继续进行迭代,仍然无法改变输出结果是0的情况,此时参数无法得到更新、进而下次输入的小批数据结果还是零、从而梯度为0、从而参数无法更新...至此陷入死循环,模型失效、激活函数失去活性,也就出现了Dead ReLU Problem。
3.1 通过调整学习率缓解Dead ReLU Problem
- 在所有的解决Dead ReLU Problem的方法中,最简单的一种方法就是调整学习率,ReLU叠加越多层越容易出现神经元活性失效,但可以简单通过降低学习率的方法来缓解神经元活性失效的问题。
- 学习率作为模型重要的超参数,会在各方面影响模型效果,学习率越小、收敛速度就越慢,而学习率过大、则又容易跳过最小值点造成模型结果震荡。对于ReLU激活函数来说,参数“稍有不慎”就容易落入输出值全为0的陷阱,因此训练过程需要更加保守,采用更小的学习率逐步迭代。当然学习率减少就必然需要增加迭代次数,但由于ReLU激活函数计算过程相对简单,增加迭代次数并不会显著增加计算量。
#设置随机种子
torch.manual_seed(420)
#核心参数
num_epochs=20
lr=0.03
#实例化模型
relu_model3=ReLU_class3()
#模型训练
train_l,test_l=model_train_test(relu_model3
,train_loader
,test_loader
,num_epochs=num_epochs
,criterion=nn.MSELoss()
,optimizer=optim.SGD
,lr=lr
,cla=False
,eva=mse_cal
)
#绘制图像,查看MSE变化情况
plt.plot(list(range(num_epochs)),train_l,label='train_mse')
plt.plot(list(range(num_epochs)),test_l,label='test_mse')
plt.title('lr=0.03')
plt.legend(loc=4)
复制代码
当学习率为0.03时,MSE在比较“高位”的位置震荡收敛,模型无效
降低学习率,提高迭代次数
#设置随机种子
torch.manual_seed(420)
#核心参数
num_epochs=40
lr=0.001
#实例化模型
relu_model3=ReLU_class3()
#模型训练
train_l,test_l=model_train_test(relu_model3
,train_loader
,test_loader
,num_epochs=num_epochs
,criterion=nn.MSELoss()
,optimizer=optim.SGD
,lr=lr
,cla=False
,eva=mse_cal
)
plt.plot(list(range(num_epochs)),train_l,label='train_mse')
plt.plot(list(range(num_epochs)),test_l,label='test_mse')
plt.title('lr=0.001')
plt.legend(loc=1)
复制代码
学习率调小之后,模型更能够避开神经元活性失效陷阱。
近期评论