深度学习之神经元活性失效与学习率优化一简介二Dead

这是我参与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)
复制代码

image.png
模型在迭代多轮之后,训练误差和测试误差都在各自取值的高位收敛了,也就是误差不随模型迭代测试增加而递减。通过简单尝试我们不难发现,此时模型对所有数据的输出结果都是0。

relu_model3(features)
复制代码

image.png

三 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')
复制代码

image.png

  • 对于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)
复制代码

image.png
当学习率为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)
复制代码

image.png
学习率调小之后,模型更能够避开神经元活性失效陷阱。