Lesson 13.2 模型拟合度概念介绍与欠拟合模型的结构调整策略
一、模型擬合度概念介紹與實驗
1.測試集的“不可知”悖論
??通過此前課程內容介紹,我們已經知道了機器學習模型主要通過模型在測試集上的運行效果來判斷模型好壞,測試集相當于是“高考”,而此前的模型訓練都相當于是在練習,但怎么樣的練習才能有效的提高高考成績,這里就存在一個“悖論”,那就是練習是為了高考,而在高考前我們永遠不知道練習是否有效,那高考對于練習的核心指導意義何在?在機器學習領域,嚴格意義上的測試集是不能參與建模的,此處不能參與建模,不僅是指在訓練模型時不能帶入測試集進行訓練,更是指當模型訓練完成之后、觀察模型在測試集上的運行結果后,也不能據此再進行模型修改(比如增加神經網絡層數),后面我們會提到,把數據帶入模型訓練是影響模型參數,而根據模型運行結果再進行模型結構調整,實際上是修改了模型超參數,不管是修改參數還是超參數,都是影響了模型建模過程,都相當于是帶入進行了建模。是的,如果通過觀察測試集結果再調整模型結構,也相當于是帶入測試集數據進行訓練,而嚴格意義上的測試集,是不能帶入模型訓練的。(這是一個有點繞的“悖論”…)
??但是,還記得我們此前說的,機器學習建模的核心目標就是提升模型的泛化能力么?而泛化能力指的是在模型未知數據集(沒帶入進行訓練的數據集)上的表現,雖然測試集只能測一次,但我們還是希望有機會能把模型帶入未知數據集進行測試,此時我們就需要一類新的數據集——驗證集。驗證集在模型訓練階段不會帶入模型進行訓練,但當模型訓練結束之后,我們會把模型帶入驗證集進行計算,通過觀測驗證集上模型運行結果,判斷模型是否要進行調整,驗證集也會模型訓練,只不過驗證集訓練的不是模型參數,而是模型超參數,關于模型參數和超參數的概念后面還會再詳細討論,當然,我們也可以把驗證集看成是應對高考的“模擬考試”,通過“模擬考試”的考試結果來調整復習策略,從而更好的應對“高考”??偟膩碚f,測試集是嚴格不能帶入訓練的數據集,在實際建模過程中我們可以先把測試集切分出來,然后“假裝這個數據集不存在”,在剩余的數據集中劃分訓練集和驗證集,把訓練集帶入模型進行運算,再把驗證集放在訓練好的模型中進行運行,觀測運行結果,再進行模型調整。
??總的來說,在模型訓練和觀測模型運行結果的過程總共涉及三類數據集,分別是訓練集、驗證集和測試集。不過由于測試集定位特殊,在一些不需要太嚴謹的場景下,有時也會混用驗證集和測試集的概念,我們常常聽到“測試集效果不好、重新調整模型”等等,都是混用了二者概念,由于以下是模擬練習過程,暫時不做測試集和驗證集的區分。在不區分驗證集和測試集的情況下,當數據集切分完成后,對于一個模型來說,我們能夠獲得兩套模型運行結果,一個是訓練集上模型效果,一個是測試集上模型效果,而這組結果,就將是整個模型優化的基礎數據。
在某些場景下,測試集確實是嚴格不可知的,比如在線提交結果的數據競賽。
2.模型擬合度概念與實驗
??在所有的模型優化問題中,最基礎的也是最核心的問題,就是關于模型擬合程度的探討與優化。根據此前的討論,模型如果能很好的捕捉總體規律,就能夠有較好的未知數據的預測效果。但限制模型捕捉總體規律的原因主要有兩點:
- 其一,樣本數據能否很好的反應總體規律
??如果樣本數據本身無法很好的反應總體規律,那建模的過程就算捕捉到了規律可能也無法適用于未知數據。舉個極端的例子,在進行反欺詐檢測時,如果要基于并未出現過欺詐案例的歷史數據來進行建模,那模型就將面臨無規律可捕捉的窘境,當然,確切的說,是無可用規律可捕捉;或者,當擾動項過大時,噪聲也將一定程度上掩蓋真實規律。 - 其二,樣本數據能反應總體規律,但模型沒有捕捉到
??如果說要解決第一種情況需要在數據獲取端多下功夫,那么如果數據能反應總體規律而模型效果不佳,則核心原因就在模型本身了。此前介紹過,機器學習模型評估主要依據模型在測試集上的表現,如果測試集效果不好,則我們認為模型還有待提升,但導致模型在測試集上效果不好的原因其實也主要有兩點,其一是模型沒捕捉到訓練集上數據的規律,其二則是模型過分捕捉訓練集上的數據規律,導致模型捕獲了大量訓練集獨有的、無法適用于總體的規律(局部規律),而測試集也是從總體中來,這些規律也不適用于測試集。前一種情況我們稱模型為欠擬合,后一種情況我們稱模型為過擬合,我們可以通過以下例子進行進一步了解:
其中,x是一個0到1之間等距分布20個點組成的ndarray,y=x+ry=\sqrt{x}+ry=x?+r,其中r是人為制造的隨機噪聲,在[-0.1,0.1]之間服從均勻分布。然后我們借助numpy的polyfit函數來進行多項式擬合,polyfit函數會根據設置的多項式階數,在給定數據的基礎上利用最小二乘法進行擬合,并返回擬合后各階系數。該函數更多相關參數詳見numpy.polynomial.polynomial.polyfit官網API講解。同時,當系數計算完成后,我們還常用ploy1d函數逆向構造多項式方程,進而利用方程求解y,該函數用法參見numpy.poly1d官網說明。
例如人為制造一個二階多項式方程然后進行二階擬合實驗
y0 = x ** 2 np.polyfit(x, y0, 2) #array([1.00000000e+00, 2.18697767e-17, 1.61618518e-17])能夠得出多項式各階系數,而根據該系數可用ploy1d逆向構造多項式方程
p = np.poly1d(np.polyfit(x, y0, 2)) print(p)1 x^2 + 2.187e-17 x + 1.616e-17
能夠看到多項式結構基本和原多項式保持一致,此時生成的p對象相當于是一個多項式方程,可通計算輸入參數的多項式輸出結果
接下來,進行多項式擬合。分別利用1階x多項式、3階x多項式和10階x多項式來擬合y。并利用圖形觀察多項式的擬合度,首先我們可定義一個輔助畫圖函數,方便后續我們將圖形畫于一張畫布中,進而方便觀察
def plot_polynomial_fit(x, y, deg):p = np.poly1d(np.polyfit(x, y, deg))t = np.linspace(0, 1, 200)plt.plot(x, y, 'ro', t, p(t), '-', t, np.sqrt(t), 'r--')其中,t為[0,1]中等距分布的100個點,而p是deg參數決定的多項式回歸擬合方程,p(t)即為擬合方程x輸入t值時多項式輸出結果,此處plot_polynomial_fit函數用于生成同時包含(x,y)原始值組成的紅色點圖、(t,p(t))組成的默認顏色的曲線圖、(t,np.sqrt(t))構成的紅色虛線曲線圖。測試3階多項式擬合結果
plot_polynomial_fit(x, y, 3)
這里需要注意(x,y)組成的紅色點圖相當于帶有噪聲的二維空間數據分布,(t, p(t))構成的藍色曲線相當于3階多項式擬合原數據集((x, y)數據集)后的結果,而原始數據集包含的客觀規律實際上是y=xy=\sqrt{x}y=x?,因此最后紅色的虛線(t, np.sqrt(t))實際上是代表紅色點集背后的客觀規律,即我們希望擬合多項式(藍色曲線)能夠盡可能的擬合代表客觀規律的紅色虛線,而不是被噪聲數據所吸引偏離紅色虛線位置,同時也不希望完全沒有捕捉到紅色曲線的規律。接下來,我們嘗試將1階擬合、3階擬合和10階擬合繪制在一張圖中。
??根據最終的輸出結果我們能夠清楚的看到,1階多項式擬合的時候藍色擬合曲線即無法捕捉數據集的分布規律,離數據集背后客觀規律也很遠,而三階多項式在這兩方面表現良好,十階多項式則在數據集分布規律捕捉上表現良好,單同樣偏離紅色曲線較遠。此時一階多項式實際上就是欠擬合,而十階多項式則過分捕捉噪聲數據的分布規律,而噪聲之所以被稱作噪聲,是因為其分布本身毫無規律可言,或者其分布規律毫無價值(如此處噪聲分布為均勻分布),因此就算十階多項式在當前訓練數據集上擬合度很高,但其捕捉到的無用規律無法推廣到新的數據集上,因此該模型在測試數據集上執行過程將會有很大誤差。即模型訓練誤差很小,但泛化誤差很大。
不難發現,過擬合產生的根本原因,還是樣本之間有“誤差”,或者不同批次的數據規律不完全一致。
因此,我們有基本結論如下:
- 模型欠擬合:訓練集上誤差較大
- 模型過擬合:訓練集上誤差較小,但測試集上誤差較大
而模型是否出現欠擬合或者過擬合,其實和模型復雜度有很大關系。我們通過上述模型不難看出,模型越復雜,越有能力捕捉訓練集上的規律,因此如果模型欠擬合,我們可以通過提高模型復雜度來進一步捕捉規律,但同時也會面臨模型過于復雜而導致過擬合的風險。
對于深度學習來說,模型復雜度和模型結構直接相關,隱藏層越多、每一層神經元越多、模型就越復雜,當然模型復雜度還和激活函數有關。模型擬合度優化是模型優化的核心,深度學習模型的一系列優化方法也基本是根據擬合度優化這一核心目標衍生出來的,接下來,我們就來看下如何通過調整模型結構來進行擬合度優化。
二、模型欠擬合實例
??首先我們來討論模型欠擬合時,通過提升模型復雜度提升模型效果的基本方法。當然,從神經網絡整體模型結構來看,提升復雜度只有兩種辦法,其一是修改激活函數,在神經元內部對加權求和匯總之后的值進行更加復雜的處理,另一種方法則是添加隱藏層,包括隱藏層層數和每一層隱藏層的神經元個數。接下來我們通過一些列實驗來查看相關效果。
??還是多元線性回歸的例子,如果是高次項方程,通過簡單的線性網絡擬合就會出現欠擬合的情況。
- 構建數據集
創建一個數據滿足y=2x12?x22y = 2x_1^2 - x_2^2y=2x12??x22?方程的分布數組
# 設置隨機數種子 torch.manual_seed(420) # 創建最高項為2的多項式回歸數據集 features, labels = tensorGenReg(w=[2, -1], bias=False, deg=2)# 繪制圖像查看數據分布 plt.subplot(121) plt.scatter(features[:, 0], labels) plt.subplot(122) plt.scatter(features[:, 1], labels) features #tensor([[-0.0070, 0.5044], # [ 0.6704, -0.3829], # [ 0.0302, 0.3826], # ..., # [-0.9164, -0.6087], # [ 0.7815, 1.2865], # [ 1.4819, 1.1390]]) # 進行數據集切分與加載 train_loader, test_loader = split_loader(features, labels)- 訓練模型
首先定義簡單線性回歸模型
class LR_class(nn.Module): # 沒有激活函數def __init__(self, in_features=2, out_features=1): # 定義模型的點線結構super(LR_class, self).__init__()self.linear = nn.Linear(in_features, out_features)def forward(self, x): # 定義模型的正向傳播規則out = self.linear(x) return out然后執行模型訓練
# 設置隨機數種子 torch.manual_seed(420) # 實例化模型 LR = LR_class()train_l = [] # 列表容器,存儲訓練誤差 test_l = [] # 列表容器,存儲測試誤差num_epochs = 20# 執行循環 for epochs in range(num_epochs):fit(net = LR,criterion = nn.MSELoss(),optimizer = optim.SGD(LR.parameters(), lr = 0.03),batchdata = train_loader,epochs = epochs)train_l.append(mse_cal(train_loader, LR).detach().numpy())test_l.append(mse_cal(test_loader, LR).detach().numpy())# 繪制圖像,查看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 = 1)
對比此前線性回歸的MSE,上述模型效果較差,且訓練誤差和測試誤差均較大(此前是0.0001),模型存在欠擬合情況,接下來考慮增加模型復雜程度。我們可以根據上述過程定義一個記錄、對比模型訓練過程訓練集、測試集誤差變化情況的函數,并寫入模塊文件。
測試模型效果
# 設置隨機數種子 torch.manual_seed(420) # 實例化模型 LR = LR_class()# 模型訓練 train_l, test_l = model_train_test(LR, train_loader,test_loader,num_epochs = 20, 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 = 1)
在神經網絡基本結構下,如何提升模型復雜度從而使得可以對多元線性回歸數據進行建模呢?首先,在激活函數僅為線性變換y=xy=xy=x情況下,增加層數并不會對結果有顯著提升,我們可以通過如下實驗驗證:
這里我們構建了一個兩層都是線性層的神經網絡,并且沒有加入激活函數。對于神經網絡來說,復雜模型的構建就是Module的疊加,核心需要注意整個傳播過程計算流程是否完整。
# 設置隨機數種子 torch.manual_seed(420) # 實例化模型 LR1 = LR_class1()# 設置隨機數種子 torch.manual_seed(420) # 實例化模型 LR1 = LR_class1()# 模型訓練 train_l, test_l = model_train_test(LR1, train_loader,test_loader,num_epochs = 20, 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 = 1)
我們發現,結果沒有顯著提升,但模型穩定性卻有所提升。對于疊加線性層的神經網絡模型來說,由于模型只是對數據仿射變換,因此并不能滿足擬合高次項的目的。也就是說,在增加模型復雜度的過程中,首先需要激活函數的配合,然后再是增加模型的層數和每層的神經元個數。
此處我們發現,模型越復雜,輸出結果越平穩,但這只是一個局部規律,其實在大多數時候,模型越復雜,輸出結果越不一定平穩。
三、激活函數性能比較
??對于激活函數來說,不同激活函數效果差異非常明顯,再初學階段,我們以學會并熟練使用常用激活函數為主,在進階階段,我們將嘗試自定義激活函數。那么接下來,就讓我們來測試不同激活函數在上述數據集中的表現。
1.常用激活函數對比
??首先我們將此前介紹的幾個常用激活函數應用在當前數據集中檢測效果。此處都是在上述線性模型基礎上再輸出層添加激活函數。也就構建包含一個隱藏層的模型,并且在隱藏層中設置不同的激活函數進行嘗試。
??正如Lesson 11中所談到的,輸出層的激活函數和隱藏層的激活函數應該分開對待,隱藏層的激活函數是為了對數據進行非線性變換,而輸出層的激活函數一般都是為了滿足某種特定的輸出結果所設計的,如softmax(是的,softmax也可以看成是激活函數)、Sigmoid等,而在其他情況下,而在當前實驗中,由于是進行回歸類問題實驗,在輸出層加入激活函數反而會影響輸出結果。
- 創建模型類
模型結構如下
相關類的定義如下
注,除了torch.方法以外,上述三個激活函數還能使用F.方法,不過根據PyTorch的相關說明,推薦使用torch.方法執行。
t = torch.randn(5) t #tensor([ 1.0870, -0.5685, 1.3383, -1.1286, 0.7049]) F.relu(t) #tensor([1.0870, 0.0000, 1.3383, 0.0000, 0.7049]) torch.relu(t) #tensor([1.0870, 0.0000, 1.3383, 0.0000, 0.7049]) F.sigmoid(t) #tensor([0.7478, 0.3616, 0.7922, 0.2444, 0.6693]) torch.sigmoid(t) #tensor([0.7478, 0.3616, 0.7922, 0.2444, 0.6693])- 實例化模型
- 定義核心參數
- 定義訓練集、測試集MSE存儲張量
??為了能夠同屬存儲多個模型的訓練誤差和測試誤差,我們這里通過張量的方式(有結構的對象)來進行存儲。其中,張量的行數就是模型列表的元素個數(模型個數),列數就是總共迭代輪數。實際運行過程中,張量的每一行用于存儲每個模型迭代不同輪數時MSE的計算結果,不同行則代表不同模型的計算結果。
mse_train = torch.zeros(len(model_l), num_epochs) mse_test = torch.zeros(len(model_l), num_epochs)- 訓練模型
訓練完成后,四組模型的訓練誤差和測試誤差都已經得到了保存,接下來通過繪制圖形觀察四組模型建模效果。
- 繪制圖形觀察結果
從當前的實驗能夠看出,相比其他激活函數,ReLU激活函數效果明顯更好。
- 激活函數性能簡介與應用場景
??根據當前的應用實踐來看,ReLU激活函數是目前使用面最廣、效果也相對更好的一種激活函數,但這并不代表tanh和Sigmoid激活函數就沒有應用場景(比如RNN、LSTM模型仍然偏愛tanh和Siamoid)。不同的激活函數擁有不同的特性,同時激活函數在復雜神經網絡中的應用也是需要優化方法支持的,并且伴隨著ReLU激活函數的不斷應用,目前ReLU已經衍生出了一個激活函數簇,相關內容我們將在下一節詳細討論。
由于模型對比將是優化實驗的常規操作,因此考慮將上述過程封裝為一個函數:
def model_comparison(model_l, name_l, 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 name_l:模型名稱序列:param train_data:訓練數據:param test_data:測試數據 :param num_epochs:迭代輪數:param criterion: 損失函數:param lr: 學習率:param cla: 是否是分類模型:return:MSE張量矩陣 """# 模型評估指標矩陣train_l = torch.zeros(len(model_l), num_epochs)test_l = torch.zeros(len(model_l), num_epochs)# 模型訓練過程for epochs in range(num_epochs):for i, model in enumerate(model_l):fit(net = model, criterion = criterion, optimizer = optimizer(model.parameters(), lr = lr), batchdata = train_data, epochs = epochs, cla = cla)train_l[i][epochs] = eva(train_data, model).detach()test_l[i][epochs] = eva(test_data, model).detach()return train_l, test_l四、構建復雜神經網絡
??在初步判斷ReLU激活函數效果好于Sigmoid激活函數和tanh激活函數之后,我們嘗試增加模型復雜度,也就是添加隱藏層來構建更加復雜的神經網絡模型。
1.ReLU激活函數疊加
??首先是ReLU激活函數的疊加,那么我們考慮添加幾層隱藏層并考慮在隱藏層中使用ReLU函數,也就是所謂的添加ReLU層。此處我們在ReLU_class1的基礎上創建ReLU_class2結構如下:
class ReLU_class2(nn.Module): def __init__(self, in_features=2, n_hidden_1=4, n_hidden_2=4, out_features=1, bias=True): super(ReLU_class2, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden_1, bias=bias)self.linear2 = nn.Linear(n_hidden_1, n_hidden_2, bias=bias)self.linear3 = nn.Linear(n_hidden_2, out_features, bias=bias)def forward(self, x): z1 = self.linear1(x)p1 = torch.relu(z1)z2 = self.linear2(p1)p2 = torch.relu(z2)out = self.linear3(p2)return out接下來,借助model_comparison函數進行模型性能測試
# 創建隨機數種子 torch.manual_seed(24) # 實例化模型 relu_model1 = ReLU_class1() relu_model2 = ReLU_class2()# 模型列表容器 model_l = [relu_model1, relu_model2] name_l = ['relu_model1', 'relu_model2']# 核心參數 num_epochs = 20 lr = 0.03train_l, test_l = model_comparison(model_l = model_l, name_l = name_l, train_data = train_loader, test_data = test_loader,num_epochs = num_epochs, criterion = nn.MSELoss(), optimizer = optim.SGD, lr = 0.03, cla = False, eva = mse_cal) # 訓練誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), train_l[i], label=name) plt.legend(loc = 1) plt.title('mse_train') # 測試誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), test_l[i], label=name) plt.legend(loc = 1) plt.title('mse_train')
??我們發現,模型效果并沒有明顯提升,反而出現了更多的波動,迭代收斂速度也有所下降。模型效果無法提升是不是因為模型還不夠復雜,如果繼續嘗試添加隱藏層會有什么效果?
??我們發現,在堆疊ReLU激活函數的過程中,模型效果并沒有朝向預想的方向發展,MSE不僅沒有越來越低,model3和model4甚至出現了模型失效的情況!這充分的說明,在當前技術手段下,模型構建并非越復雜越好。同時我們能夠清晰的看到,伴隨模型復雜度增加,模型收斂速度變慢、收斂過程波動增加、甚至有可能出現模型失效的情況。但同時我們又知道,深度學習本身就是一種構建復雜模型的方法,并且其核心價值就在于使用深度神經網絡處理海量數據。從根本上來說,當前實驗復雜模型出現問題并不是算法理論本身出了問題,而是我們缺乏了解決這些問題的“技術手段”,只有掌握了這些“技術手段”之后,才能真正構建運行高效、泛化能力強的模型。而這些技術手段,就是模型優化方法。其實這也從側面說明了優化算法的重要性。
??關于優化方法的內容我們將在后續課程逐步介紹。在此之前,我們還需要看下Sigmoid激活函數tanh激活函數在堆疊過程中的表現,由此發現不同激活函數在深層次神經網絡運行時存在的不同問題。
此處ReLU激活函數疊加后出現的模型失效問題,也就是Dead ReLU Problem。
2.Sigmoid激活函數疊加
??同樣,我們再構建擁有兩個隱藏層、三個隱藏層和四個隱藏層的神經網絡。
class Sigmoid_class2(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, out_features=1): super(Sigmoid_class2, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.sigmoid(z1)z2 = self.linear2(p1)p2 = torch.sigmoid(z2)out = self.linear3(p2)return outclass Sigmoid_class3(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, out_features=1): super(Sigmoid_class3, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, n_hidden3)self.linear4 = nn.Linear(n_hidden3, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.sigmoid(z1)z2 = self.linear2(p1)p2 = torch.sigmoid(z2)z3 = self.linear3(p2)p3 = torch.sigmoid(z3)out = self.linear4(p3)return outclass Sigmoid_class4(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, n_hidden4=4, out_features=1): super(Sigmoid_class4, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, n_hidden3)self.linear4 = nn.Linear(n_hidden3, n_hidden4)self.linear5 = nn.Linear(n_hidden4, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.sigmoid(z1)z2 = self.linear2(p1)p2 = torch.sigmoid(z2)z3 = self.linear3(p2)p3 = torch.sigmoid(z3)z4 = self.linear4(p3)p4 = torch.sigmoid(z4)out = self.linear5(p4)return out然后在相同的數據集上測試模型效果
# 創建隨機數種子 torch.manual_seed(24) # 實例化模型 sigmoid_model1 = Sigmoid_class1() sigmoid_model2 = Sigmoid_class2() sigmoid_model3 = Sigmoid_class3() sigmoid_model4 = Sigmoid_class4()# 模型列表容器 model_l = [sigmoid_model1, sigmoid_model2, sigmoid_model3, sigmoid_model4] name_l = ['sigmoid_model1', 'sigmoid_model2', 'sigmoid_model3', 'sigmoid_model4']# 核心參數 num_epochs = 50 lr = 0.03train_l, test_l = model_comparison(model_l = model_l, name_l = name_l, train_data = train_loader,test_data = test_loader,num_epochs = num_epochs, criterion = nn.MSELoss(), optimizer = optim.SGD, lr = lr, cla = False, eva = mse_cal)# 訓練誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), train_l[i], label=name) plt.legend(loc = 1) plt.title('mse_train') # 測試誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), test_l[i], label=name) plt.legend(loc = 1) plt.title('mse_test')
??sigmoid激活函數的簡單疊加也出現了很多問題,雖然沒有像ReLU疊加一樣出現大幅MSE升高的情況,但仔細觀察,不難發現,對于model1、model2、model3來說,伴隨模型復雜增加,模型效果沒有提升,但收斂速度卻下降的很嚴重,而model4更是沒有收斂到其他幾個模型的MSE,問題不小。不過相比ReLU激活函數,整體收斂過程確實稍顯穩定,而Sigmoid也是老牌激活函數,在2000年以前,是最主流的激活函數。
此處Sigmoid激活函數堆疊后出現的問題,本質上就是梯度消失所導致的問題。
3.tanh激活函數疊加
??最后,我們再來看下tanh激活函數疊加后的模型效果
class tanh_class2(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, out_features=1): super(tanh_class2, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.tanh(z1)z2 = self.linear2(p1)p2 = torch.tanh(z2)out = self.linear3(p2)return outclass tanh_class3(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, out_features=1): super(tanh_class3, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, n_hidden3)self.linear4 = nn.Linear(n_hidden3, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.tanh(z1)z2 = self.linear2(p1)p2 = torch.tanh(z2)z3 = self.linear3(p2)p3 = torch.tanh(z3)out = self.linear4(p3)return outclass tanh_class4(nn.Module): def __init__(self, in_features=2, n_hidden1=4, n_hidden2=4, n_hidden3=4, n_hidden4=4, out_features=1): super(tanh_class4, self).__init__()self.linear1 = nn.Linear(in_features, n_hidden1)self.linear2 = nn.Linear(n_hidden1, n_hidden2)self.linear3 = nn.Linear(n_hidden2, n_hidden3)self.linear4 = nn.Linear(n_hidden3, n_hidden4)self.linear5 = nn.Linear(n_hidden4, out_features) def forward(self, x): z1 = self.linear1(x)p1 = torch.tanh(z1)z2 = self.linear2(p1)p2 = torch.tanh(z2)z3 = self.linear3(p2)p3 = torch.tanh(z3)z4 = self.linear4(p3)p4 = torch.tanh(z4)out = self.linear5(p4)return out# 創建隨機數種子 torch.manual_seed(42) # 實例化模型 tanh_model1 = tanh_class1() tanh_model2 = tanh_class2() tanh_model3 = tanh_class3() tanh_model4 = tanh_class4()# 模型列表容器 model_l = [tanh_model1, tanh_model2, tanh_model3, tanh_model4] name_l = ['tanh_model1', 'tanh_model2', 'tanh_model3', 'tanh_model4']# 核心參數 num_epochs = 50 lr = 0.03 train_l, test_l = model_comparison(model_l = model_l, name_l = name_l, train_data = train_loader,test_data = test_loader,num_epochs = num_epochs, criterion = nn.MSELoss(), optimizer = optim.SGD, lr = lr, cla = False, eva = mse_cal)# 訓練誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), train_l[i], label=name) plt.legend(loc = 1) plt.title('mse_train') # 測試誤差 for i, name in enumerate(name_l):plt.plot(list(range(num_epochs)), test_l[i], label=name) plt.legend(loc = 1) plt.title('mse_test')
??tanh激活函數疊加效果中規中矩,在model1到model2的過程效果明顯向好,MSE基本一致、收斂速度基本一致、但收斂過程穩定性較好,也證明模型結果較為可信,而model3、model4則表現出了和前面兩種激活函數在疊加過程中所出現的類似的問題,當然對于tanh來說,最明顯的問題是出現了劇烈波動,甚至出現了“跳躍點”。
此處tanh激活函數堆疊所導致的迭代過程劇烈波動的問題,也被稱為迭代不平穩,需要優化迭代過程來解決。
五、神經網絡結構選擇策略
1.參數和超參數
??經過上述建模過程,我們可以對模型結構的選擇略進行總結。
??在實際建模過程中,神經網絡的模型結構是影響建模結果至關重要的因素。但對于一個數據集,構建幾層神經網絡、每一層設置多少個神經元,卻不是一個存在唯一最優解的參數,雖然我們希望能夠有個數學過程幫我們直接確定模型結構,但由于影響因素較多,很多時候模型在確定模型結構上,還是一個首先根據經驗設置模型結構、然后再根據實際建模效果不斷調整的過程。
關于參數和超參數的說明:
??在機器學習中,參數其實分為兩類,其一是參數,其二則是超參數,一個影響模型的變量是參數還是超參數,核心區別就在于這個變量的取值能否通過一個嚴謹的數學過程求出,如果可以,我們就稱其為參數,如果不行,我們就稱其為超參數。典型的,如簡單線性回歸中的自變量的權重,我們通過最小二乘法或者梯度下降算法,能夠求得一組全域最優解,因此自變量的權重就是參數,類似的,復雜神經網絡中的神經元連接權重也是參數。但除此以外,還有一類影響模型效果的變量卻無法通過構建數學方程、然后采用優化算法算得最優解,典型的如訓練集和測試集劃分比例、神經網絡的層數、每一層神經元個數等等,這些變量同樣也會影響模型效果,但由于無法通過數學過程求得最優解,很多時候我們都是憑借經驗設置數值,然后根據建模結果再進行手動調節,這類變量我們稱其為超參數。
??不難發現,在實際機器學習建模過程中超參數出現的場景并不比參數出現的場景少,甚至很多時候,超參數的邊界也和我們如何看待一個模型息息相關,例如,如果我們把“選什么模型”也看成是一個最終影響建模效果的變量,那這個變量也是一個超參數。也就是說,超參數也就是由“人來決策”的部分場合,而這部分也就是體現算法工程師核心競爭力的環節。
??當然,就像此前介紹的那樣,參數通過優化算法計算出結果,而機器學習發展至今,也出現了很多輔助超參數調節的工具,比如網格搜索(grid search)、AutoML等,但哪怕是利用工具調整超參數,無數前輩在長期實踐積累下來的建模經驗仍然是彌足珍貴的,也是我們需要不斷學習、不斷理解,當然,更重要的是,所有技術人經驗的積累是一個整體,因此也是需要我們參與分享的。
2.神經網絡模型結構選擇策略
而一般來說在模型結構選擇方面,根據經驗,有如下基本結論:
- 層數選擇方面
- 三層以內:模型效果會隨著層數增加而增加;
- 三層至六層:隨著層數的增加,模型的穩定性迭代的穩定性會受到影響,并且這種影響是隨著層數增加“指數級”增加的,此時我們就需要采用一些優化方法對輸入數據、激活函數、損失函數和迭代過程進行優化,一般來說在六層以內的神經網絡在通用的優化算法配合下,是能夠收斂至一個較好的結果的;
- 六層以上:在模型超過六層之后,優化方法在一定程度上仍然能夠輔助模型訓練,但此時保障模型正常訓練的更為核心的影響因素,就變成了數據量本身和算力。神經網絡模型要迭代收斂至一個穩定的結果,所需的epoch是隨著神經網絡層數增加而增加的,也就是說神經網絡模型越復雜,收斂所需迭代的輪數就越多,此時所需的算力也就越多。而另一方面,伴隨著模型復雜度增加,訓練所需的數據量也會增加,如果是復雜模型應用于小量樣本數據,則極有可能會出現“過擬合”的問題從而影響模型的泛化能力。當然,伴隨著模型復雜度提升、所需訓練數據增加,對模型優化所采用的優化算法也會更加復雜。也就是說,六層以內神經網絡應對六層以上的神經網絡模型,我們需要更多的算力支持、更多的數據量、以及更加復雜的優化手段支持。
??因此,對于大多數初中級算法工程師來說,如果不是借助已有經典模型而是自己構建模型的話,不建議搭建六層以上的神經網絡模型。而關于通用優化方法,將在下一節開始逐步介紹。
- 每一層神經元個數選擇方面:當然輸入層的神經元個數就是特征個數,而輸出層神經元個數,如果是回歸類問題或者是邏輯回歸解決二分類問題,輸出層就只有一個神經元,而如果是多分類問題,輸出層神經元個數就是類別總數。而隱藏層神經元個數,可以按照最多不超過輸入特征的2-4倍進行設置,當然默認連接方式是全連接,每一個隱藏層可以設置相同數量的神經元。其實對于神經元個數設置來說,后期是有調整空間的,哪怕模型創建過程神經元數量有些“飽和”,我們后期我們可以通過丟棄法(優化方法的一種)對隱藏層神經元個數和連接方式進行修改。對于某些非結構化數據來說,隱藏層神經元個數也會根據數據情況來進行設置,如神經元數量和圖像關鍵點數量匹配等。
另外,值得一提的是,經驗是對過去的一般情況的總結,并不代表所有情況,同時也不代表對未來的預測。同時,上述規則適用于自定義神經網絡模型的情況,很多針對某一類問題的經典深度學習架構會有單獨的規則。
3.激活函數使用的單一性
??同時,對于激活函數的交叉使用,我們需要知道,通常來說,是不會出現多種激活函數應用于一個神經網絡中的情況的。主要原因并不是因為模型效果就一定會變差,而是如果幾種激活函數效果類似,那么交叉使用幾種激活函數其實效果和使用一種激活函數區別不大,而如果幾種激活函數效果差異非常明顯,那么這幾種激活函數的堆加就會使得模型變得非常不可控。此前的實驗讓我們深刻體會優化算法的必要性,但目前工業界所掌握的、針對激活函數的優化算法都是針對某一種激活函數來使用的,激活函數的交叉使用會令這些優化算法失效。因此,盡管機器學習模型是“效果為王”,但在基礎理論沒有進一步突破之前,不推薦在一個神經網路中使用多種激活函數。
總結
以上是生活随笔為你收集整理的Lesson 13.2 模型拟合度概念介绍与欠拟合模型的结构调整策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Lesson 13.1 深度学习建模目标
- 下一篇: Lesson13【加餐】 损失函数的随机