Lesson 13.4 Dead ReLU Problem与学习率优化
Lesson 13.4 Dead ReLU Problem與學習率優化
??和Sigmoid、tanh激活函數不同,ReLU激活函數的疊加并不會出現梯度消失或者梯度爆炸,但ReLU激活函數中使得部分數值歸零的特性卻會導致另外一個嚴重的問——Dead ReLU Problem,也被稱為神經元活性失效問題。
一、Dead ReLU Problem成因分析
1.Dead ReLU Problem直接表現
??首先我們通過實驗來觀察神經元活性失效問題(Dead ReLU Problem)在建模過程中的直接表現。其實在上一節中,最后出現的ReLU疊加模型在迭代多次后在MSE取值高位收斂的情況,其實就是出現了神經元活性失效所導致的問題
# 設置隨機數種子 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。
2.Dead ReLU Problem成因分析
2.1 Dead 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輸出結果為零,則反向傳播結果、也就是各層的梯度,也都是零。
我們進一步通過舉例說明,現在有模型基本結構如下
設w1為第一層傳播的權重,w2為第二層傳播的權重,并且w1的第一列對應連接隱藏層第一個神經元的權重,w1的第二列對應連接隱藏層第二個神經元的權重,f為輸入的特征張量,并且只有一條數據
第一次向前傳播過程如下:
# 線性變換 f2 = torch.mm(f, w1) f2 #tensor([[-2., -4.]], grad_fn=<MmBackward>) # 激活函數處理 f3 = torch.relu(f2) f3 #tensor([[0., 0.]], grad_fn=<ReluBackward0>) # 輸出結果 out = torch.mm(f3, w2) out #tensor([[0.]], grad_fn=<MmBackward>)l為f的真實標簽,則損失函數和反向傳播過程如下:
l = torch.tensor([[3.]]) l #tensor([[3.]]) loss = F.mse_loss(out, l) loss #tensor(9., grad_fn=<MseLossBackward>) loss.backward()而此時w1、w2的梯度如下:
w1.grad w2.grad #tensor([[0., 0.], # [0., 0.]]) #tensor([[0.], # [0.]])我們發現,當某條數據在模型中的輸出結果為0時,反向傳播后各層參數的梯度也全為0,此時參數將無法通過迭代更新。而進一步的,如果在某種參數情況下,整個訓練數據集輸入模型之后輸出結果都是0,則在小批量梯度下降的情況下,每次再挑選出一些數據繼續進行迭代,仍然無法改變輸出結果是0的情況,此時參數無法得到更新、進而下次輸入的小批數據結果還是零、從而梯度為0、從而參數無法更新…至此陷入死循環,模型失效、激活函數失去活性,也就出現了Dead ReLU Problem。
當然,上述過程可以由如下數學過程說明,假設模型預測值:y^=ReLU(X?w1)?w2\hat y = ReLU(X * w_1) * w_2y^?=ReLU(X?w1?)?w2?
并且,出現Dead ReLU Problem的時候,某一組w1w_1w1?恰好使得ReLU輸出結果為0,因此y^=0\hat y = 0y^?=0,而此時損失函數為:
loss=MSE=∑((y^?y)2)Nloss = MSE = \frac{\sum ((\hat y - y) ^ 2)}{N} loss=MSE=N∑((y^??y)2)?
根據鏈式法則,此時梯度為:grad=(?loss?w1,?loss?w2)=(?loss?y^?y^?w1,?loss?y^?y^?w2)\begin{aligned} grad &= ( \frac{\partial loss}{\partial w_1}, \frac{\partial loss}{\partial w_2}) \\ &= (\frac{\partial loss}{\partial \hat y} \frac{\partial \hat y}{\partial w_1}, \frac{\partial loss}{\partial \hat y} \frac{\partial \hat y}{\partial w_2})\\ \end{aligned} grad?=(?w1??loss?,?w2??loss?)=(?y^??loss??w1??y^??,?y^??loss??w2??y^??)?
由于y^\hat yy^?恒為0(為一個常量)(所有y^\hat yy^?都是0),由ReLU激活函數導函數特性可知,y^\hat yy^?對任何自變量偏導也為0,此時grad也就為0,w1、w2w_1、w_2w1?、w2?不會再更新。而參數不更新,y^\hat yy^?仍然為0,梯度還是為0,如此往復,就出現了上述死循環,也就是Dead ReLU Problem。
2.2 Dead ReLU Problem發生概率
當然,我們也可略微進行一些拓展。試想以下,出現Dead ReLU Problem問題的概率,其實是伴隨ReLU層的增加而增加的。如果是兩層ReLU層,模型結構如下:
向前傳播過程中,模型輸出結果為:
y^=ReLU(ReLU(X?w1)?w2)?w3\hat y = ReLU(ReLU(X * w_1) * w_2) * w_3y^?=ReLU(ReLU(X?w1?)?w2?)?w3?
不難發現,在兩層ReLU嵌套的情況下,y^\hat yy^?取值為0的概率就更大了,出現Dead ReLU Problem的概率也就更高了。而同時,根據各層參數的梯度計算公式我們也能夠發現,只要其中任意一層輸出結果是0,則所有層參數的梯度均為0。
grad1=?loss?y^?w3?f(F(X?w1)?w2)?w2?f(X?w1)?Xgrad_1 = \frac{\partial loss}{\partial \hat y} \cdot w_3 \cdot f(F(X*w_1)*w_2) \cdot w_2 \cdot f(X * w_1) \cdot X grad1?=?y^??loss??w3??f(F(X?w1?)?w2?)?w2??f(X?w1?)?Xgrad2=?loss?y^?w3?f(F(X?w1)?w2)?F(X?w1)grad_2 = \frac{\partial loss}{\partial \hat y} \cdot w_3 \cdot f(F(X*w_1)*w_2) \cdot F(X * w_1) grad2?=?y^??loss??w3??f(F(X?w1?)?w2?)?F(X?w1?)grad3=?loss?y^?F(F(X?w1)?w2)grad_3 = \frac{\partial loss}{\partial \hat y} \cdot F(F(X * w_1) * w_2) grad3?=?y^??loss??F(F(X?w1?)?w2?)
最終,我們可以通過如下表達式判別ReLU激活函數是否失效
當然,如果模型是帶入偏差進行的建模,出現Dead ReLU Problem的時候模型輸出結果恒為bias的取值。
二、通過調整學習率緩解Dead ReLU Problem
??在所有的解決Dead ReLU Problem的方法中,最簡單的一種方法就是調整學習率。盡管我們知道,ReLU疊加越多層越容易出現神經元活性失效,但我們可以簡單通過降低學習率的方法來緩解神經元活性失效的問題。甚至可以說這是一種通用且有效的方法。
??學習率作為模型重要的超參數,會在各方面影響模型效果,此前我們曾介紹學習率越小、收斂速度就越慢,而學習率過大、則又容易跳過最小值點造成模型結果震蕩。對于ReLU激活函數來說,參數“稍有不慎”就容易落入輸出值全為0的陷阱,因此訓練過程需要更加保守,采用更小的學習率逐步迭代。當然學習率減少就必然需要增加迭代次數,但由于ReLU激活函數計算過程相對簡單,增加迭代次數并不會顯著增加計算量。
我們發現學習率調小之后,模型更能夠避開神經元活性失效陷阱。關于更多學習率調整策略,我們會在后續完整介紹。
三、ReLU激活函數特性理解
??至此,我們也可以進一步理解ReLU激活函數的實際作用。可以這么說,ReLU激活函數的實際作用是“選擇性更新部分參數”,回顧上述公式,我們不難發現,當激活函數是ReLU激活函數時,以上述例子為例,無論是第一層接收到的數值(X?w1X*w_1X?w1?)小于0,還是第二層接收到的數值(F(X?w1)?w2F(X*w_1)*w_2F(X?w1?)?w2?)小于0,都會導致三層參數的梯度為0,同時,哪怕是第一層接收到的數據全是0,也會導致所有參數本輪不被更新。而在實際深度學習建模過程中,我們是采用小批量梯度下降算法來進行梯度計算,而如果某一批的數據輸出結果為0,則當前迭代結束時參數不變,也就相當于模型采用了“有選擇”的數據進行參數更新——只“選擇”了那些輸出結果不為0的數據進行參數訓練。同時我們需要知道的是,這一輪某一批數據沒被選擇,不代表下一輪這批數據仍然不被選擇(因為參數會變化,因而輸出結果也會發生變化),因此我們可以理解為每一輪都帶入不同數據進行參數訓練,從而最終完成模型訓練。
??并且,從梯度消失和梯度爆炸角度考慮,ReLU激活函數擁有更加優異的特性。我們都知道,ReLU激活函數的導函數取值要么是1要么是0,當導函數取值為1時,能夠保證各層參數的梯度在計算時不受因層數變化而累乘的導函數影響(因為導函數取值都為1)。
值得注意的是,每次帶入不同批次數據訓練,或者說每次有選擇性的忽略部分數據,就相當于進行了“非線性”的變換。
關于“隨機性”的作用,其實我們已經見過很多次了,在集成模型中,我們將在一定隨機性條件下構建的、彼此不同的基分類器進行集成,從而創建一個效果明顯好于單個基分類器的集成模型;在SGD中,我們采用每次帶入隨機部分數據的方法進行梯度下降迭代,從而使得迭代過程能夠跳出局部最小值點;而在ReLU激活函數中,我們隨機挑選部分參數進行迭代,從而完成數據的“非線性”轉化,進而保證模型本身的有效性。
也正因如此,ReLU激活函數也被稱為非飽和激活函數
relu非線性轉化就是選擇數據進行學習
我們可以觀察上一節模型訓練結束后各層參數的梯度
# 觀察各層梯度 for m in relu_model3.modules():if isinstance(m, nn.Linear):print(m.weight.grad) #tensor([[-1.2105, -1.1560], # [-1.2854, 0.4011], # [ 0.5821, 0.9778], # [ 1.2699, 1.5803]]) #tensor([[ 0.0000, 0.0000, 0.0000, 0.0000], # [ 0.0000, 0.0000, 0.0000, 0.0000], # [-1.7618, -0.2564, 0.1256, -4.4533], # [ 0.7966, 0.1120, 0.6588, 2.0890]]) #tensor([[ 0.0000, 0.0000, 0.6559, 0.4969], # [ 0.0000, 0.0000, 0.7240, 0.5485], # [ 0.0000, 0.0000, -0.7708, -0.5839], # [ 0.0000, 0.0000, -0.7633, -0.5783]]) #tensor([[-0.3015, -0.4115, -0.5745, -0.5258]]) weights_vp(relu_model3, att="grad")
能夠看出,模型各層仍然處在學習狀態,雖然存在梯度不均的狀態,但在學習率非常小的情況下整體表現仍然較為平穩。不過需要注意的是,對于ReLU激活函數來說,每一層梯度分布的小提琴圖會很大程度受到梯度0值的影響,并且每一次迭代完成后是否取0值都會發生變化,從而影響小提琴圖對真實情況反應的準確程度。
四、nn.Sequential快速建模方法及nn.init模型參數自定義方法
??在討論如何解決上述激活函數疊加問題之前,我們先補充兩個基礎工具,其一是使用nn.Sequential進行模型的快速構建,其二則是使用nn.init來進行模型參數修改。其中關于模型參數修改的相關方法,也是支撐本節優化方法實踐的核心。
1.nn.Sequential快速建模方法介紹
??首先補充關于使用nn.Sequential進行快速建模的方法介紹。在此前的建模環節,我們都是通過完整的創建模型類、通過定義init方法和forward方法來確定模型的基本結構、傳播方式和激活函數。除了這種模型定義方法外,PyTorch還支持使用nn.Sequential來快速,在借助nn.Sequential進行模型構建過程中,我們只需要將每一層神經元連接方法和激活函數作為參數輸入nn.Sequential即可,具體流程如下:
# 設置隨機數種子 torch.manual_seed(25)# 構建上述LR_ReLU_test模型 relu_test = nn.Sequential(nn.Linear(2, 2, bias=False), nn.ReLU(), nn.Linear(2, 1, bias=False))在上述模型定義過程中,relu_test相當于已經實例化之后的模型
list(relu_test.parameters()) #[Parameter containing: # tensor([[ 0.3561, -0.4343], # [-0.6182, 0.5823]], requires_grad=True), # Parameter containing: # tensor([[-0.1658, -0.2843]], requires_grad=True)]而此時的實例化,是nn.Sequential類的實例化,也就是說,通過nn.Sequential創建的模型本質上都是nn.Sequential的一個實例。
isinstance(relu_test, nn.Sequential) #True # 而此前創建的模型都是我們所創建的類的實例 isinstance(relu_model3, ReLU_class3) #True而上述nn.Sequential所創建的模型結構,就相當于是兩層全連接神經網絡,并且隱藏層使用ReLU進行處理。其中需要注意的是,nn.ReLU()單獨使用時就相當于ReLU函數,而放在nn.Sequential中就等價于對某一層的輸出結果進行ReLU處理。
r1 = nn.ReLU() t = torch.tensor([1., -1]) t #tensor([ 1., -1.]) r1(t) #tensor([1., 0.]) torch.relu(t) #tensor([1., 0.])當然,通過nn.Sequential定義的模型也可以執行向前傳播過程
f = torch.tensor([[1, 2.]], requires_grad = True) f #tensor([[1., 2.]], requires_grad=True) relu_test.forward(f) #tensor([[-0.1553]], grad_fn=<MmBackward>)我們可以手動驗證
w1 = list(relu_test.parameters())[0].t() # 第一層傳播參數 w1 #tensor([[ 0.3561, -0.6182], # [-0.4343, 0.5823]], grad_fn=<TBackward>) w2 = list(relu_test.parameters())[1].t() # 第二層傳播參數 w2 #tensor([[-0.1658], # [-0.2843]], grad_fn=<TBackward>) torch.mm(torch.relu(torch.mm(f, w1)), w2) #tensor([[-0.1553]], grad_fn=<MmBackward>)當然,如果進一步將rele的參數手動設置為此前設置的w1和w2,就可以復現ReLU激活函數的活性失效例子。那如何才能在手動修改模型參數值呢?我們將在下面一小節進行補充。
總而言之,我們不難發現,利用nn.Sequential進行模型創建在模型結構相對簡單時可以大幅減少代碼量,并且模型效果和先通過定義類、再進行實例化的模型效果相同,但該方法在定義高度復雜的模型、或者定義更加靈活的模型時就顯得力不從心了。因此,對于新手,推薦先掌握利用類定義模型的方法,再掌握利用nn.Sequential定義模型的方法。
2.模型參數自定義方法
??接下來,繼續補充關于手動設置模型初始參數及模型參數共享的方法。首先,對于模型參數來說,parameters返回結果是個生成器(generator),通過list轉化后會生成一個由可微張量構成的list。
- 通過修改可微張量方法修改參數
因此,我們可以通過修改可微張量數值的方法對其進行修改。在Lesson 12中,我們介紹了三種修改可微張量數值的方法,這里我們直接使用.data的方法對其進行修改:
# 修改目標 w1 = torch.tensor([[0., 0], [-1, -2]]) w2 = torch.tensor([1., -1]).reshape(-1, 1) w1 w2 #tensor([[ 0., 0.], # [-1., -2.]]) #tensor([[ 1.], # [-1.]]) list(relu_test.parameters())[0].data = w1.t() list(relu_test.parameters())[1].data = w2.t() # 查看修改結果 list(relu_test.parameters()) #[Parameter containing: # tensor([[ 0., -1.], # [ 0., -2.]], requires_grad=True), # Parameter containing: # tensor([[ 1., -1.]], requires_grad=True)]然后即可執行向前傳播
f = torch.tensor([[1, 2.]]) f #tensor([[1., 2.]]) # 模型輸出結果 out = relu_test.forward(f) out #tensor([[0.]], grad_fn=<MmBackward>) # 真實標簽 l = torch.tensor([[3.]]) l #tensor([[3.]])接下來計算損失函數
loss = F.mse_loss(out, l) loss #tensor(9., grad_fn=<MseLossBackward>)進行反向傳播
loss.backward()查看模型參數梯度
list(relu_test.parameters())[0].grad #tensor([[0., 0.], # [0., 0.]]) list(relu_test.parameters())[1].grad #tensor([[0., 0.]])至此也驗證了和此前手動實現的相同結果。
- 使用init方法創建滿足某種分布的參數
??除了通過手動方法修改參數值以外,我們還可以使用nn.init方法來對模型參數進行修改。
# 重新設置初始化模型參數取值 # 設置隨機數種子 torch.manual_seed(25)# 構建上述LR_ReLU_test模型 relu_test = nn.Sequential(nn.Linear(2, 2, bias=False), nn.ReLU(), nn.Linear(2, 1, bias=False))list(relu_test.parameters()) #[Parameter containing: # tensor([[ 0.3561, -0.4343], # [-0.6182, 0.5823]], requires_grad=True), # Parameter containing: # tensor([[-0.1658, -0.2843]], requires_grad=True)](1).nn.init.uniform_方法,新生成的參數服從均勻分布
nn.init.uniform_(relu_test.parameters(), 0, 1) #AttributeError: 'generator' object has no attribute 'uniform_' list(relu_test.parameters())[0] #Parameter containing: #tensor([[ 0.3561, -0.4343], # [-0.6182, 0.5823]], requires_grad=True) nn.init.uniform_(list(relu_test.parameters())[0], 0, 1) # 設置參數值為均勻分布在0,1區間內的隨機數 #Parameter containing: #tensor([[0.0481, 0.3497], # [0.3520, 0.9528]], requires_grad=True)當然,和此前一樣,帶有下劃線的函數都是能夠直接修改對象本身的
list(relu_test.parameters())[0] #Parameter containing: #tensor([[0.0481, 0.3497], # [0.3520, 0.9528]], requires_grad=True)(2).nn.init.normal_方法,新生成的參數服從正態分布
nn.init.normal_(list(relu_test.parameters())[0], 0, 1) # 服從均值為0、標準差為1的正態分布 #Parameter containing: #tensor([[ 0.0827, 0.5799], # [ 0.0578, -0.2979]], requires_grad=True)就相當于手動修改,然后使用size參數,最后需要令其可導并替換原始參數值。
torch.normal(0, 1, size = list(relu_test.parameters())[0].size()) #tensor([[-1.5217, 0.6919], # [ 0.8875, -0.3946]])(3).nn.init.constant_方法,新生成的參數值為某一常數
nn.init.constant_(list(relu_test.parameters())[0], 1) # 參數全為1 #Parameter containing: #tensor([[1., 1.], # [1., 1.]], requires_grad=True)和下述表達式等效,然后再令其可導并替換原始參數值。
torch.full_like(list(relu_test.parameters())[0], 1) #tensor([[1., 1.], # [1., 1.]])??當然,上述過程并不復雜,并且相同的修改目標,使用手動方式也能實現。對于nn.init方法來說,最核心的使用場景是能夠創建服從特殊分布、具備一定特性的、能夠輔助模型迭代收斂的初始參數。相關方法我們將在下一小節詳細介紹。
總結
以上是生活随笔為你收集整理的Lesson 13.4 Dead ReLU Problem与学习率优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Lesson 13.3 梯度不平稳性与G
- 下一篇: Lesson 13.5 Xavier方法