怎么自己做砍價(jià)網(wǎng)站市場(chǎng)營銷互聯(lián)網(wǎng)營銷
1.引言
在本文中,我們將探討神經(jīng)網(wǎng)絡(luò)的優(yōu)化與初始化技術(shù)。隨著神經(jīng)網(wǎng)絡(luò)深度的增加,我們會(huì)遇到多種挑戰(zhàn)。最關(guān)鍵的是確保網(wǎng)絡(luò)中梯度流動(dòng)的穩(wěn)定性,否則可能會(huì)遭遇梯度消失或梯度爆炸的問題。因此,我們將深入探討以下兩個(gè)核心概念:網(wǎng)絡(luò)參數(shù)的初始化和優(yōu)化算法的選擇。
本文的前半部分,我們將介紹不同的參數(shù)初始化方法,從最基本的初始化策略開始,逐步深入到當(dāng)前在極深網(wǎng)絡(luò)中應(yīng)用的高級(jí)技術(shù)。在后半部分,我們將聚焦于優(yōu)化算法的比較,分析SGD、動(dòng)量SGD以及Adam這幾種優(yōu)化器的性能差異。
首先,讓我們開始導(dǎo)入所需的標(biāo)準(zhǔn)庫。
## 標(biāo)準(zhǔn)庫
import os
import json
import math
import numpy as np
import copy## 繪圖所需導(dǎo)入
import matplotlib.pyplot as plt
from matplotlib import cm
%matplotlib inline
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg', 'pdf') # 用于導(dǎo)出
import seaborn as sns
sns.set()## 進(jìn)度條
from tqdm.notebook import tqdm## PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as data
import torch.optim as optim
#我們將使用與教程3相同的set_seed函數(shù),以及路徑變量DATASET_PATH和CHECKPOINT_PATH。如有必要,請(qǐng)調(diào)整路徑。# 數(shù)據(jù)集下載存放的文件夾路徑(例如MNIST)
DATASET_PATH = "../data"
# 預(yù)訓(xùn)練模型保存的文件夾路徑
CHECKPOINT_PATH = "../saved_models/tutorial4"# 設(shè)置種子的函數(shù)
def set_seed(seed):np.random.seed(seed)torch.manual_seed(seed)if torch.cuda.is_available():torch.cuda.manual_seed(seed)torch.cuda.manual_seed_all(seed)
set_seed(42)# 確保在GPU上的所有操作都是確定性的(如果使用)以實(shí)現(xiàn)可復(fù)現(xiàn)性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False# 獲取將在此筆記本中使用整個(gè)過程中使用的設(shè)備
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
使用設(shè)備 cuda:0
##在本文的最后部分,我們將使用三種不同的優(yōu)化器訓(xùn)練模型。以下是這些模型的預(yù)訓(xùn)練版本下載鏈接。import urllib.request
from urllib.error import HTTPError
# 存儲(chǔ)本教程預(yù)訓(xùn)練模型的Github URL
base_url = "https://raw.githubusercontent.com/phlippe/saved_models/main/tutorial4/"
# 需要下載的文件
pretrained_files = ["FashionMNIST_SGD.config", "FashionMNIST_SGD_results.json", "FashionMNIST_SGD.tar","FashionMNIST_SGDMom.config", "FashionMNIST_SGDMom_results.json", "FashionMNIST_SGDMom.tar","FashionMNIST_Adam.config", "FashionMNIST_Adam_results.json", "FashionMNIST_Adam.tar" ]
# 如果檢查點(diǎn)路徑不存在,則創(chuàng)建
os.makedirs(CHECKPOINT_PATH, exist_ok=True)# 對(duì)于每個(gè)文件,檢查它是否已經(jīng)存在。如果不存在,嘗試下載。
for file_name in pretrained_files:file_path = os.path.join(CHECKPOINT_PATH, file_name)if not os.path.isfile(file_path):file_url = base_url + file_nameprint(f"正在下載 {file_url}...")try:urllib.request.urlretrieve(file_url, file_path)except HTTPError as e:print("下載過程中出現(xiàn)問題。請(qǐng)嘗試從GDrive文件夾下載文件,或聯(lián)系作者,并附上包括以下錯(cuò)誤的完整輸出:\n", e)
2.準(zhǔn)備工作
在本文中,我們將使用一個(gè)深度全連接網(wǎng)絡(luò),與我們之前的文章類似。我們還將再次將網(wǎng)絡(luò)應(yīng)用于FashionMNIST,我們首先加載FashionMNIST數(shù)據(jù)集:
from torchvision.datasets import FashionMNIST
from torchvision import transforms# 應(yīng)用于每張圖片的轉(zhuǎn)換 => 首先將它們轉(zhuǎn)換為張量,然后使用均值為0和標(biāo)準(zhǔn)差為1進(jìn)行歸一化
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.2861,), (0.3530,))
])# 加載訓(xùn)練數(shù)據(jù)集。我們需要將其分割為訓(xùn)練部分和驗(yàn)證部分
train_dataset = FashionMNIST(root=DATASET_PATH, train=True, transform=transform, download=True)
train_set, val_set = torch.utils.data.random_split(train_dataset, [50000, 10000])# 加載測(cè)試集
test_set = FashionMNIST(root=DATASET_PATH, train=False, transform=transform, download=True)# 我們定義一組數(shù)據(jù)加載器,我們稍后可以用于不同的目的。
# 注意,對(duì)于實(shí)際訓(xùn)練模型,我們將使用具有較小批量大小的不同數(shù)據(jù)加載器。
train_loader = data.DataLoader(train_set, batch_size=1024, shuffle=True, drop_last=False)
val_loader = data.DataLoader(val_set, batch_size=1024, shuffle=False, drop_last=False)
test_loader = data.DataLoader(test_set, batch_size=1024, shuffle=False, drop_last=False)
與之前的文章相比,我們更改了歸一化轉(zhuǎn)換transforms.Normalize的參數(shù)。現(xiàn)在歸一化的設(shè)計(jì)是讓我們?cè)谙袼厣汐@得預(yù)期的均值為0和標(biāo)準(zhǔn)差為1。這將特別適用于我們下面將要討論的初始化問題,因此我們?cè)谶@里進(jìn)行更改。應(yīng)當(dāng)指出,在大多數(shù)分類任務(wù)中,兩種歸一化技術(shù)(介于-1和1之間或均值為0和標(biāo)準(zhǔn)差為1)都已被證明效果良好。我們可以通過在原始圖像上確定均值和標(biāo)準(zhǔn)差來計(jì)算歸一化參數(shù):
print("Mean", (train_dataset.data.float() / 255.0).mean().item())
print("Std", (train_dataset.data.float() / 255.0).std().item())
輸出顯示為:
Mean 0.2860923707485199
Std 0.3530242443084717
我們可以通過查看單個(gè)批次的統(tǒng)計(jì)數(shù)據(jù)來驗(yàn)證轉(zhuǎn)換:
imgs, _ = next(iter(train_loader))
print(f"Mean: {imgs.mean().item():5.3f}")
print(f"Standard deviation: {imgs.std().item():5.3f}")
print(f"Maximum: {imgs.max().item():5.3f}")
print(f"Minimum: {imgs.min().item():5.3f}")
輸出:
Mean: 0.002
Standard deviation: 1.001
Maximum: 2.022
Minimum: -0.810
請(qǐng)注意,最大值和最小值不再是1和-1,而是向正值偏移。這是因?yàn)镕ashionMNIST包含許多黑色像素,與MNIST類似。接下來,我們將創(chuàng)建一個(gè)線性神經(jīng)網(wǎng)絡(luò)。
class BaseNetwork(nn.Module):def __init__(self, act_fn, input_size=784, num_classes=10, hidden_sizes=[512, 256, 256, 128]):"""輸入:act_fn - 應(yīng)該在網(wǎng)絡(luò)中作為非線性使用的激活函數(shù)的對(duì)象。input_size - 輸入圖像的像素尺寸num_classes - 我們想要預(yù)測(cè)的類別數(shù)量hidden_sizes - 一個(gè)整數(shù)列表,指定神經(jīng)網(wǎng)絡(luò)中隱藏層的大小"""super().__init__()# 根據(jù)指定的隱藏大小創(chuàng)建網(wǎng)絡(luò)layers = []layer_sizes = [input_size] + hidden_sizesfor layer_index in range(1, len(layer_sizes)):layers += [nn.Linear(layer_sizes[layer_index-1], layer_sizes[layer_index]),act_fn]layers += [nn.Linear(layer_sizes[-1], num_classes)]self.layers = nn.ModuleList(layers) # 模塊列表將模塊列表注冊(cè)為子模塊(例如,用于參數(shù))self.config = {"act_fn": act_fn.__class__.__name__, "input_size": input_size, "num_classes": num_classes, "hidden_sizes": hidden_sizes}def forward(self, x):x = x.view(x.size(0), -1)for l in self.layers:x = l(x)return x
對(duì)于激活函數(shù),我們使用PyTorch的torch.nn庫而不是自己實(shí)現(xiàn)。當(dāng)然,我們也定義了一個(gè)Identity激活函數(shù)。盡管這種激活函數(shù)會(huì)大大限制網(wǎng)絡(luò)的建模能力,但我們將在我們的初始化討論的第一步中使用它(為了簡化)。
class Identity(nn.Module):def forward(self, x):return xact_fn_by_name = {"tanh": nn.Tanh,"relu": nn.ReLU,"identity": Identity
}
最后,我們定義了一些繪圖函數(shù),我們將在討論中使用它們。這些函數(shù)幫助我們
(1)可視化網(wǎng)絡(luò)內(nèi)部的權(quán)重/參數(shù)分布,
(2)可視化不同層的參數(shù)接收的梯度,以及
(3)激活值,即線性層的輸出。
# 繪制值的分布圖
def plot_dists(val_dict, color="C0", xlabel=None, stat="count", use_kde=True):columns = len(val_dict) # 圖表的列數(shù)等于val_dict的鍵的數(shù)量fig, ax = plt.subplots(1, columns, figsize=(columns*3, 2.5)) # 創(chuàng)建子圖fig_index = 0for key in sorted(val_dict.keys()): # 遍歷val_dict的鍵key_ax = ax[fig_index % columns] # 獲取當(dāng)前的子圖軸sns.histplot(val_dict[key], ax=key_ax, color=color, bins=50, stat=stat, # 繪制直方圖kde=use_kde and ((val_dict[key].max()-val_dict[key].min())>1e-8)) # 如果有方差則繪制核密度估計(jì)key_ax.set_title(f"{key} " + (r"(%i $\to$ %i)" % (val_dict[key].shape[1], val_dict[key].shape[0]) if len(val_dict[key].shape) > 1 else "")) # 設(shè)置標(biāo)題if xlabel is not None:key_ax.set_xlabel(xlabel) # 設(shè)置x軸標(biāo)簽fig_index += 1fig.subplots_adjust(wspace=0.4) # 調(diào)整子圖之間的間隔return fig# 可視化模型權(quán)重分布
def visualize_weight_distribution(model, color="C0"):weights = {}for name, param in model.named_parameters(): # 遍歷模型的參數(shù)if name.endswith(".bias"): # 如果是偏置,則跳過continuekey_name = f"Layer {name.split('.')[1]}" # 為權(quán)重創(chuàng)建鍵名weights[key_name] = param.detach().view(-1).cpu().numpy() # 將權(quán)重轉(zhuǎn)換為numpy數(shù)組# 繪圖fig = plot_dists(weights, color=color, xlabel="Weight vals") # 使用plot_dists函數(shù)繪制權(quán)重分布圖fig.suptitle("Weight distribution", fontsize=14, y=1.05) # 設(shè)置圖表標(biāo)題plt.show() # 顯示圖表plt.close() # 關(guān)閉圖表# 可視化模型梯度分布
def visualize_gradients(model, color="C0", print_variance=False):# 設(shè)置模型為評(píng)估模式model.eval()small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False) # 創(chuàng)建數(shù)據(jù)加載器imgs, labels = next(iter(small_loader)) # 獲取一批數(shù)據(jù)imgs, labels = imgs.to(device), labels.to(device) # 將數(shù)據(jù)移動(dòng)到設(shè)備上# 將一批數(shù)據(jù)通過網(wǎng)絡(luò)前向傳播,并計(jì)算權(quán)重的梯度model.zero_grad() # 清空梯度preds = model(imgs) # 前向傳播loss = F.cross_entropy(preds, labels) # 計(jì)算交叉熵?fù)p失loss.backward() # 反向傳播計(jì)算梯度# 限制可視化為權(quán)重參數(shù),不包括偏置,以減少圖表數(shù)量grads = {name: params.grad.view(-1).cpu().clone().numpy() for name, params in model.named_parameters() if "weight" in name}model.zero_grad() # 清空梯度# 繪圖fig = plot_dists(grads, color=color, xlabel="Grad magnitude") # 使用plot_dists函數(shù)繪制梯度分布圖fig.suptitle("Gradient distribution", fontsize=14, y=1.05) # 設(shè)置圖表標(biāo)題plt.show() # 顯示圖表plt.close() # 關(guān)閉圖表if print_variance: # 如果需要打印方差for key in sorted(grads.keys()): # 遍歷梯度字典的鍵print(f"{key} - Variance: {np.var(grads[key])}") # 打印方差# 可視化模型激活值分布
def visualize_activations(model, color="C0", print_variance=False):model.eval() # 設(shè)置模型為評(píng)估模式small_loader = data.DataLoader(train_set, batch_size=1024, shuffle=False) # 創(chuàng)建數(shù)據(jù)加載器imgs, labels = next(iter(small_loader)) # 獲取一批數(shù)據(jù)imgs, labels = imgs.to(device), labels.to(device) # 將數(shù)據(jù)移動(dòng)到設(shè)備上# 將一批數(shù)據(jù)通過網(wǎng)絡(luò)前向傳播,并計(jì)算權(quán)重的梯度feats = imgs.view(imgs.shape[0], -1) # 重塑特征activations = {}with torch.no_grad(): # 不計(jì)算梯度for layer_index, layer in enumerate(model.layers): # 遍歷模型的每一層feats = layer(feats) # 應(yīng)用層if isinstance(layer, nn.Linear): # 如果是線性層activations[f"Layer {layer_index}"] = feats.view(-1).detach().cpu().numpy() # 將激活值轉(zhuǎn)換為numpy數(shù)組# 繪圖fig = plot_dists(activations, color=color, stat="density", xlabel="Activation vals") # 使用plot_dists函數(shù)繪制激活值分布圖fig.suptitle("Activation distribution", fontsize=14, y=1.05) # 設(shè)置圖表標(biāo)題plt.show() # 顯示圖表plt.close() # 關(guān)閉圖表if print_variance: # 如果需要打印方差for key in sorted(activations.keys()): # 遍歷激活值字典的鍵print(f"{key} - Variance: {np.var(activations[key])}") # 打印方差
3.初始化
在深入討論神經(jīng)網(wǎng)絡(luò)的初始化問題之前,有必要指出,關(guān)于這一主題,網(wǎng)絡(luò)上已經(jīng)有許多精彩的博客文章,例如deeplearning.ai提供的資源,或者那些更側(cè)重于數(shù)學(xué)分析的文章。如果在閱讀完本教程后仍有疑惑,我們建議您也瀏覽一下這些博客文章以獲得更深入的理解。
初始化神經(jīng)網(wǎng)絡(luò)時(shí),我們希望其具備一些特定的屬性。首先,輸入數(shù)據(jù)的方差應(yīng)能通過整個(gè)網(wǎng)絡(luò)傳遞到輸出層,以保證輸出神經(jīng)元具有相似的標(biāo)準(zhǔn)差。如果我們?cè)诰W(wǎng)絡(luò)深層發(fā)現(xiàn)方差逐漸消失,那么模型將難以優(yōu)化,因?yàn)橄乱粚拥妮斎雽⒆兊脦缀醯韧谝粋€(gè)恒定值。同樣地,如果方差隨著網(wǎng)絡(luò)深度的增加而增大,那么梯度可能會(huì)變得非常大,導(dǎo)致數(shù)值穩(wěn)定性問題。其次,我們希望在初始化時(shí)各層的梯度分布具有相同的方差。如果第一層得到的梯度遠(yuǎn)小于最后一層,我們可能就會(huì)在選擇合適的學(xué)習(xí)速率時(shí)遇到困難。
為了尋找合適的初始化方法,我們首先以一個(gè)沒有激活函數(shù)的線性神經(jīng)網(wǎng)絡(luò)作為起點(diǎn)進(jìn)行分析,即網(wǎng)絡(luò)中僅使用恒等激活函數(shù)。之所以這樣做,是因?yàn)椴煌募せ詈瘮?shù)對(duì)初始化方法有特定的要求,我們可以根據(jù)所使用的激活函數(shù)調(diào)整初始化策略。
model = BaseNetwork(act_fn=Identity()).to(device)
3.1 常數(shù)初始化
接下來,我們考慮一種最簡單的初始化方法——常數(shù)初始化。直觀上,將所有權(quán)重設(shè)置為零并不理想,因?yàn)檫@會(huì)導(dǎo)致傳播的梯度也為零。但是,如果我們將所有權(quán)重設(shè)置為一個(gè)接近零的非零常數(shù),情況會(huì)如何呢?為了探究這一點(diǎn),我們可以編寫一個(gè)函數(shù)來實(shí)現(xiàn)這一初始化,并可視化梯度的分布情況。
定義了一個(gè)名為const_init
的函數(shù),它接受一個(gè)模型和一個(gè)默認(rèn)為0的常數(shù)值c
,將模型中所有的參數(shù)(權(quán)重)填充為這個(gè)常數(shù)值。然后,我們調(diào)用這個(gè)函數(shù)將模型的權(quán)重初始化為0.005,接著使用visualize_gradients
和visualize_activations
函數(shù)來可視化梯度和激活值的分布,并打印出它們的方差。這有助于我們理解在這種初始化策略下,網(wǎng)絡(luò)的梯度和激活值的行為。
def const_init(model, c=0.0):for name, param in model.named_parameters():param.data.fill_(c)const_init(model, c=0.005)
visualize_gradients(model)
visualize_activations(model, print_variance=True)
Layer 0 - Variance: 2.058276
Layer 2 - Variance: 13.489119
Layer 4 - Variance: 22.100567
Layer 6 - Variance: 36.209572
Layer 8 - Variance: 14.831439
從我們的觀察來看,只有第一層和最后一層展現(xiàn)出了多樣化的梯度分布,而中間的三層則顯示出所有權(quán)重具有相同梯度的現(xiàn)象(注意,這個(gè)值并不為零,但往往非常接近零)。如果用相同值初始化的參數(shù)最終獲得了相同的梯度,這就意味著這些參數(shù)的值將始終一致。這樣的結(jié)果會(huì)讓我們網(wǎng)絡(luò)中的這一層失去作用,實(shí)際上將我們網(wǎng)絡(luò)的參數(shù)數(shù)量減少到了單一的一個(gè)值。因此,我們不能采用常數(shù)值初始化的方法來訓(xùn)練我們的網(wǎng)絡(luò)。
3.2.關(guān)于方差的恒定性
在上述實(shí)驗(yàn)中,我們已經(jīng)發(fā)現(xiàn)單一的常數(shù)值初始化策略是行不通的。那么,如果我們改為從諸如高斯分布的某種概率分布中隨機(jī)采樣來初始化參數(shù),情況會(huì)怎樣呢?最直接的方法可能是為網(wǎng)絡(luò)中的所有層選擇一個(gè)相同的方差值。接下來,我們將實(shí)現(xiàn)這種方法,并可視化各層的激活分布情況。
def var_init(model, std=0.01):for name, param in model.named_parameters():param.data.normal_(std=std)var_init(model, std=0.01)
visualize_activations(model, print_variance=True)
在神經(jīng)網(wǎng)絡(luò)的層與層之間,激活值的方差呈現(xiàn)出逐漸減小的趨勢(shì),到了最后一層,方差幾乎趨近于零。這種情況下,一個(gè)可能的解決辦法是增加標(biāo)準(zhǔn)差的數(shù)值。通過提高初始化時(shí)的標(biāo)準(zhǔn)差,我們可以嘗試維持網(wǎng)絡(luò)深層的激活方差,避免其在傳播過程中消失。
var_init(model, std=0.1)
visualize_activations(model, print_variance=True)
通過使用更高的標(biāo)準(zhǔn)差進(jìn)行初始化,我們可以觀察到網(wǎng)絡(luò)各層激活值的分布情況,特別是它們的方差,以評(píng)估這種策略是否有效。這種方法可能有助于解決深層網(wǎng)絡(luò)中的梯度消失問題,但同時(shí)也要警惕不要導(dǎo)致梯度爆炸,這需要我們?cè)趯?shí)踐中仔細(xì)調(diào)整和平衡。
3.3.如何找到合適的初始化值
從我們之前的實(shí)驗(yàn)中,我們可以看到需要從某個(gè)概率分布中對(duì)權(quán)重進(jìn)行采樣,但具體選擇哪個(gè)分布我們還不確定。下一步,我們將嘗試從激活值分布的角度出發(fā),尋找最優(yōu)的初始化方法。為此,我們提出兩個(gè)要求:
(1)激活值的均值應(yīng)該為零。
(2)激活值的方差應(yīng)該在每一層都保持不變。
假設(shè)我們要為以下層設(shè)計(jì)一個(gè)初始化方法:要設(shè)計(jì)一個(gè)滿足上述兩個(gè)要求的初始化方法,我們需要考慮權(quán)重矩陣和激活函數(shù)的特性。對(duì)于一個(gè)全連接層,如果使用恒等激活函數(shù)(即線性激活函數(shù)),輸出的均值和方差將取決于輸入的均值和方差以及權(quán)重矩陣。為了使激活值的均值為零,我們可以選擇一個(gè)合適的均值。而為了保持激活值的方差在每一層都相同,我們需要選擇一個(gè)合適的標(biāo)準(zhǔn)差。
一種常見的方法是使用與輸入維度的平方根成反比的標(biāo)準(zhǔn)差。這樣,無論輸入的維度如何變化,權(quán)重的標(biāo)準(zhǔn)差都會(huì)相應(yīng)調(diào)整,以保持激活值的方差大致相同。這種方法通常被稱為Xavier初始化或Glorot初始化。
我們的目標(biāo)是讓每個(gè)元素的方差與輸入的方差相同,即每個(gè)權(quán)重更新后的方差應(yīng)該保持與輸入數(shù)據(jù)的方差一致。這是為了確保在多層網(wǎng)絡(luò)中,信息能夠穩(wěn)定地從前層傳遞到后層,避免出現(xiàn)梯度消失或爆炸的問題。
在數(shù)學(xué)上,如果我們考慮一個(gè)全連接層 y = W x + b , y ∈ R d y , x ∈ R d x y=Wx+b,y\in\mathbb{R}^{d_y},x\in\mathbb{R}^{d_x} y=Wx+b,y∈Rdy?,x∈Rdx?,其輸出可以表示為 Var ( y i ) = Var ( x i ) = σ x 2 \text{Var}(y_i)=\text{Var}(x_i)=\sigma_x^{2} Var(yi?)=Var(xi?)=σx2?,,其中 是權(quán)重矩陣 W W W, 是輸入 X X X, 是偏置 b b b。對(duì)于激活函數(shù) σ \sigma σ ,激活輸出 a a a以表示為 a = σ ( z ) a = \sigma(z) a=σ(z) 。我們希望 a a a的方差保持與 x x x的方差相同。
對(duì)于偏置項(xiàng) b b b,通常初始化為0,因?yàn)樗鼈儾挥绊懠せ钪档姆讲?。?duì)于權(quán)重 W W W,如果我們假設(shè)輸入 x x x的方差為 σ x 2 \sigma_x^2 σx2?,并且我們希望輸出 y y y的方差也為 σ x 2 \sigma_x^2 σx2?,那么我們可以通過以下方式初始化權(quán)重:
W ~ N ( 0 , σ x 2 n ) W \sim \mathcal{N}(0, \frac{\sigma_x^2}{n}) W~N(0,nσx2??)
這里是輸入特征的數(shù)量。這種初始化方法確保了在沒有激活函數(shù)的情況下,輸出 的方差與輸入 的方差相同。
如果使用激活函數(shù),我們需要根據(jù)激活函數(shù)的特性調(diào)整初始化策略。例如,對(duì)于ReLU激活函數(shù),He初始化是一種流行的選擇,它使用稍微不同的公式來初始化權(quán)重,以保持激活值的方差。
在實(shí)踐中,我們通常會(huì)使用現(xiàn)成的初始化方法,如Xavier初始化(適用于tanh激活函數(shù))或He初始化(適用于ReLU激活函數(shù)),這些方法已經(jīng)考慮了保持激活值方差的需要。
接下來,我們需要計(jì)算用于初始化權(quán)重參數(shù)所需的方差。在計(jì)算過程中,我們需要使用以下方差規(guī)則:給定兩個(gè)獨(dú)立的隨機(jī)變量,它們的乘積的方差是 Var [ X Y ] = E [ X 2 ] ? E [ Y 2 ] ? ( E [ X ] ? E [ Y ] ) 2 \text{Var}[XY] = \text{E}[X^2] \cdot \text{E}[Y^2] - (\text{E}[X] \cdot \text{E}[Y])^2 Var[XY]=E[X2]?E[Y2]?(E[X]?E[Y])2 (這里 x x x和 y y y不是指特定的隨機(jī)變量,而是任意隨機(jī)變量)。
所需權(quán)重 W W W的方差 Var [ W i j ] \text{Var}[W_{ij}] Var[Wij?] 計(jì)算如下:
y i = ∑ j w i j x j 單個(gè)輸出神經(jīng)元的計(jì)算(不含偏置項(xiàng)) Var ( y i ) = σ x 2 = Var ( ∑ j w i j x j ) = ∑ j Var ( w i j x j ) 輸入和權(quán)重是彼此獨(dú)立的。? = ∑ j Var ( w i j ) ? Var ( x j ) 方差規(guī)則(見上文),期望值為零 = d x ? Var ( w i j ) ? Var ( x j ) 對(duì)于所有的 d x 元素,方差相等 = σ x 2 ? d x ? Var ( w i j ) ? Var ( w i j ) = σ W 2 = 1 d x \begin{split}\begin{split} y_i & = \sum_{j} w_{ij}x_{j}\hspace{10mm}\\\text{單個(gè)輸出神經(jīng)元的計(jì)算(不含偏置項(xiàng))}\\ \text{Var}(y_i) = \sigma_x^{2} & = \text{Var}\left(\sum_{j} w_{ij}x_{j}\right)\\ & = \sum_{j} \text{Var}(w_{ij}x_{j}) \hspace{10mm}\\\text{輸入和權(quán)重是彼此獨(dú)立的。 }\\ & = \sum_{j} \text{Var}(w_{ij})\cdot\text{Var}(x_{j}) \hspace{10mm}\\\text{方差規(guī)則(見上文),期望值為零}\\ & = d_x \cdot \text{Var}(w_{ij})\cdot\text{Var}(x_{j}) \hspace{10mm}\\\text{對(duì)于所有的$d_x$元素,方差相等}\\ & = \sigma_x^{2} \cdot d_x \cdot \text{Var}(w_{ij})\\ \Rightarrow \text{Var}(w_{ij}) = \sigma_{W}^2 & = \frac{1}{d_x}\\ \end{split}\end{split} yi?單個(gè)輸出神經(jīng)元的計(jì)算(不含偏置項(xiàng))Var(yi?)=σx2?輸入和權(quán)重是彼此獨(dú)立的。?方差規(guī)則(見上文),期望值為零對(duì)于所有的dx?元素,方差相等?Var(wij?)=σW2??=j∑?wij?xj?=Var(j∑?wij?xj?)=j∑?Var(wij?xj?)=j∑?Var(wij?)?Var(xj?)=dx??Var(wij?)?Var(xj?)=σx2??dx??Var(wij?)=dx?1???
基于上述理論,我們的權(quán)重初始化策略應(yīng)該是使用一個(gè)具有適當(dāng)方差的分布。具體來說,權(quán)重的方差應(yīng)該是輸入維度倒數(shù)的方差。這樣的初始化有助于保持網(wǎng)絡(luò)各層激活值的方差大致相同,從而有助于梯度在網(wǎng)絡(luò)中的穩(wěn)定流動(dòng)。
def equal_var_init(model):for name, param in model.named_parameters():if name.endswith(".bias"): # 如果是偏置項(xiàng),則初始化為0param.data.fill_(0)else:# 對(duì)權(quán)重使用特定的標(biāo)準(zhǔn)差進(jìn)行正態(tài)分布初始化# 標(biāo)準(zhǔn)差為1除以輸入特征數(shù)量的平方根param.data.normal_(std=1.0/math.sqrt(param.shape[1]))# 應(yīng)用Equal Variance Initialization到模型
equal_var_init(model)# 可視化權(quán)重分布
visualize_weight_distribution(model)# 可視化激活值分布,并打印每層激活值的方差
visualize_activations(model, print_variance=True)
Layer 0 - Variance: 1.020319
Layer 2 - Variance: 1.049295
Layer 4 - Variance: 1.031418
Layer 6 - Variance: 1.025792
Layer 8 - Variance: 0.872356
正如我們所預(yù)期的,方差確實(shí)在各層之間保持恒定。請(qǐng)注意,我們的初始化方法并不限制我們只能使用正態(tài)分布,而是允許使用任何具有0均值和 2 n x + n next \frac{2}{n_x + n_{\text{next}}} nx?+nnext?2?或者 1 d x \frac{1}{d_x} dx?1?方差的其他分布。通常你會(huì)看到使用均勻分布進(jìn)行初始化。使用均勻分布而不是正態(tài)分布的一個(gè)小小好處是,我們可以排除初始化非常大或非常小的權(quán)重的可能性。
除了激活值的方差之外,我們希望穩(wěn)定的另一個(gè)方差是梯度的方差。這確保了深層網(wǎng)絡(luò)的穩(wěn)定優(yōu)化。結(jié)果表明,我們可以從 Δ x = W Δ y \Delta x=W\Delta y Δx=WΔy開始進(jìn)行與上述相同的計(jì)算,并得出我們應(yīng)該使用 1 d y \frac{1}{d_y} dy?1?來初始化我們的層的結(jié)論,其中 是輸出神經(jīng)元的數(shù)量。你可以將這個(gè)計(jì)算作為練習(xí)來做,或者在這個(gè)博客文章中查看詳盡的解釋。作為兩種約束之間的折衷,Glorot和Bengio(2010年)提議使用這兩個(gè)值的調(diào)和平均值。這引導(dǎo)我們得到了眾所周知的Xavier初始化:
W ~ N ( 0 , 2 d x + d y ) W\sim \mathcal{N}\left(0,\frac{2}{d_x+d_y}\right) W~N(0,dx?+dy?2?)
如果我們使用均勻分布來初始化權(quán)重,我們會(huì)這樣設(shè)置:
W ~ U [ ? 6 d x + d y , 6 d x + d y ] W\sim U\left[-\frac{\sqrt{6}}{\sqrt{d_x+d_y}}, \frac{\sqrt{6}}{\sqrt{d_x+d_y}}\right] W~U[?dx?+dy??6??,dx?+dy??6??]
def xavier_init(model):for name, param in model.named_parameters():if name.endswith(".bias"):param.data.fill_(0)else:bound = math.sqrt(6)/math.sqrt(param.shape[0]+param.shape[1])param.data.uniform_(-bound, bound)xavier_init(model)
visualize_gradients(model, print_variance=True)
visualize_activations(model, print_variance=True)
layers.0.weight - Variance: 0.000436
layers.2.weight - Variance: 0.000747
layers.4.weight - Variance: 0.001149
layers.6.weight - Variance: 0.001744
layers.8.weight - Variance: 0.017655
Layer 0 - Variance: 1.216592
Layer 2 - Variance: 1.719161
Layer 4 - Variance: 1.714506
Layer 6 - Variance: 2.224779
Layer 8 - Variance: 5.297660
Xavier初始化方法旨在保持網(wǎng)絡(luò)中梯度和激活值方差的一致性。我們注意到,輸出層的方差之所以顯著增加,是因?yàn)檩斎雽雍洼敵鰧拥木S度存在較大差異。例如,輸入層可能有1024個(gè)神經(jīng)元,而輸出層可能僅有10個(gè)神經(jīng)元。目前,我們的討論假設(shè)了激活函數(shù)是線性的。引入非線性激活函數(shù),如tanh或ReLU,會(huì)改變激活值的分布,進(jìn)而影響梯度的方差。
在基于tanh的網(wǎng)絡(luò)中,一個(gè)普遍的假設(shè)是,在訓(xùn)練初期,對(duì)于接近零的小值,tanh函數(shù)可以近似為線性函數(shù)。這意味著,在訓(xùn)練的早期階段,我們不需要調(diào)整初始化策略的計(jì)算。然而,隨著訓(xùn)練的進(jìn)行,權(quán)重的更新可能會(huì)導(dǎo)致激活值的分布發(fā)生變化,從而使得tanh的非線性特性變得更加顯著。
為了驗(yàn)證我們的初始化策略是否適用于非線性激活函數(shù),我們可以在訓(xùn)練的早期階段檢查激活值的分布。如果激活值主要集中在tanh的線性區(qū)域(即接近零點(diǎn)),那么我們的初始化方法可能仍然有效。如果激活值分布遠(yuǎn)離零點(diǎn),我們可能需要考慮調(diào)整初始化策略,以適應(yīng)激活函數(shù)的非線性特性。
model = BaseNetwork(act_fn=nn.Tanh()).to(device)
xavier_init(model)
visualize_gradients(model, print_variance=True)
visualize_activations(model, print_variance=True)
layers.0.weight - Variance: 0.000016
layers.2.weight - Variance: 0.000027
layers.4.weight - Variance: 0.000036
layers.6.weight - Variance: 0.000049
layers.8.weight - Variance: 0.000455
Layer 0 - Variance: 1.295969
Layer 2 - Variance: 0.583388
Layer 4 - Variance: 0.291432
Layer 6 - Variance: 0.265237
Layer 8 - Variance: 0.274929
盡管隨著深度的增加方差有所減小,但很明顯激活值的分布更加集中在低值上。因此,如果我們進(jìn)一步加深網(wǎng)絡(luò),方差將穩(wěn)定在0.25左右。因此,我們可以得出結(jié)論,Xavier初始化對(duì)于Tanh網(wǎng)絡(luò)效果很好。但是對(duì)于ReLU網(wǎng)絡(luò)呢?在這里,我們不能采用之前對(duì)于小值時(shí)非線性趨近線性的假設(shè)。ReLU激活函數(shù)(按期望)將一半的輸入設(shè)置為0,因此輸入的期望值也不是零。然而,只要 W = 0 W=0 W=0 和 b = 0 b=0 b=0 ,輸出的期望值就是零。ReLU初始化的計(jì)算與恒等激活函數(shù)不同之處在于確定權(quán)重的標(biāo)準(zhǔn)差 Var ( w i j x j ) \text{Var}(w_{ij}x_{j}) Var(wij?xj?):
Var ( w i j x j ) = E [ w i j 2 ] ? = Var ( w i j ) E [ x j 2 ] ? E [ w i j ] 2 ? = 0 E [ x j ] 2 = Var ( w i j ) E [ x j 2 ] \text{Var}(w_{ij}x_{j})=\underbrace{\mathbb{E}[w_{ij}^2]}_{=\text{Var}(w_{ij})}\mathbb{E}[x_{j}^2]-\underbrace{\mathbb{E}[w_{ij}]^2}_{=0}\mathbb{E}[x_{j}]^2=\text{Var}(w_{ij})\mathbb{E}[x_{j}^2] Var(wij?xj?)==Var(wij?) E[wij2?]??E[xj2?]?=0 E[wij?]2??E[xj?]2=Var(wij?)E[xj2?]
如果我們現(xiàn)在假設(shè) 是前一層經(jīng)過ReLU激活函數(shù)的輸出(即 ,我們可以按照以下方式計(jì)算期望值:
E [ x 2 ] = E [ max ? ( 0 , y ~ ) 2 ] = 1 2 E [ y ~ 2 ] y ~ 是以零為中心且對(duì)稱的 = 1 2 Var ( y ~ ) \begin{split}\begin{split} \mathbb{E}[x^2] & =\mathbb{E}[\max(0,\tilde{y})^2]\\ & =\frac{1}{2}\mathbb{E}[{\tilde{y}}^2]\hspace{2cm}\tilde{y}\text{ 是以零為中心且對(duì)稱的}\\ & =\frac{1}{2}\text{Var}(\tilde{y}) \end{split}\end{split} E[x2]?=E[max(0,y~?)2]=21?E[y~?2]y~??是以零為中心且對(duì)稱的=21?Var(y~?)??
由于ReLU函數(shù)的定義為 max ? ( 0 , y ~ ) \max(0, \tilde{y}) max(0,y~?),它將所有負(fù)值置為0,而所有正值保持不變。因此,對(duì)于輸入 y ~ \tilde{y} y~?的任意小的正期望 μ y ~ \mu_{\tilde{y}} μy~??,輸出 y y y 的期望 μ y ~ \mu_{\tilde{y}} μy~??將是:
μ y = E [ y ] = E [ max ? ( 0 , y ~ ) ] \mu_y = \mathbb{E}[y] = \mathbb{E}[\max(0, \tilde{y})] μy?=E[y]=E[max(0,y~?)]
由于 y ~ \tilde{y} y~?的負(fù)部分被置為0,只有當(dāng) y ~ \tilde{y} y~?大于0時(shí),它才對(duì)期望有貢獻(xiàn)。假設(shè) y ~ \tilde{y} y~?的概率密度函數(shù)是對(duì)稱的,那么其正負(fù)部分的期望將抵消,只有正值部分對(duì)期望有貢獻(xiàn)。因此,我們可以簡化計(jì)算為:
μ y = ∫ 0 ∞ y ~ p ( y ~ ) d y ~ \mu_y = \int_0^\infty \tilde{y} p(\tilde{y}) d\tilde{y} μy?=∫0∞?y~?p(y~?)dy~?
這里 p ( y ~ ) p(\tilde{y}) p(y~?)是 的概率密度函數(shù)。如果 是從標(biāo)準(zhǔn)正態(tài)分布 初始化的,那么:
μ y = σ y ~ 2 π \mu_y = \sigma_{\tilde{y}} \sqrt{\frac{2}{\pi}} μy?=σy~??π2??
這個(gè)結(jié)果表明,即使輸入 y ~ \tilde{y} y~?的期望是0,經(jīng)過ReLU激活函數(shù)后,輸出 的期望也會(huì)是一個(gè)正的小數(shù)值。這個(gè)正值來自于正態(tài)分布的正尾部分的積分。
在初始化權(quán)重時(shí),我們需要考慮到這一點(diǎn),以確保在ReLU激活下,網(wǎng)絡(luò)的輸出和梯度的期望保持在合理的范圍內(nèi)。這就是為什么He初始化(也稱為Kaiming初始化)為ReLU激活專門設(shè)計(jì)了權(quán)重的初始化策略。
因此,我們發(fā)現(xiàn)在方程中有一個(gè)額外的1/2因子,所以我們期望的權(quán)重方差變?yōu)?。這給我們提供了Kaiming初始化(見He, K. 等人 (2015) 的論文)。請(qǐng)注意,Kaiming初始化不使用輸入和輸出大小之間的調(diào)和平均值。在他們的論文(第2.2節(jié),反向傳播,最后一段)中,他們爭論說使用 或 都可以在整個(gè)網(wǎng)絡(luò)中得到穩(wěn)定的梯度,并且只依賴于網(wǎng)絡(luò)的整體輸入和輸出大小。因此,我們這里只使用輸入 :
def kaiming_init(model):for name, param in model.named_parameters():if name.endswith(".bias"):param.data.fill_(0)elif name.startswith("layers.0"): # The first layer does not have ReLU applied on its inputparam.data.normal_(0, 1/math.sqrt(param.shape[1]))else:param.data.normal_(0, math.sqrt(2)/math.sqrt(param.shape[1]))model = BaseNetwork(act_fn=nn.ReLU()).to(device)
kaiming_init(model)
visualize_gradients(model, print_variance=True)
visualize_activations(model, print_variance=True)
layers.0.weight - Variance: 0.000075
layers.2.weight - Variance: 0.000108
layers.4.weight - Variance: 0.000185
layers.6.weight - Variance: 0.000444
layers.8.weight - Variance: 0.005548
Layer 0 - Variance: 1.012342
Layer 2 - Variance: 1.092432
Layer 4 - Variance: 1.268176
Layer 6 - Variance: 1.193706
Layer 8 - Variance: 1.760064
Kaiming初始化通過特別考慮ReLU激活函數(shù)的特性,確保了在基于ReLU的網(wǎng)絡(luò)中權(quán)重的方差能夠在每一層保持穩(wěn)定。這種初始化方法對(duì)于保持深層網(wǎng)絡(luò)在訓(xùn)練過程中梯度的穩(wěn)定性至關(guān)重要。
然而,對(duì)于其他變體的ReLU激活函數(shù),比如Leaky-ReLU,其中負(fù)值不會(huì)被置為零,而是乘以一個(gè)小的正斜率(例如0.01),我們需要對(duì)Kaiming初始化的方差因子進(jìn)行調(diào)整。這是因?yàn)長eaky-ReLU的輸出不會(huì)像標(biāo)準(zhǔn)的ReLU那樣有一半的零值,因此期望值和方差的計(jì)算會(huì)有所不同。
PyTorch框架提供了一個(gè)內(nèi)置函數(shù) calculate_gain
,它可以根據(jù)激活函數(shù)的不同自動(dòng)計(jì)算所需的初始化增益。這個(gè)函數(shù)可以自動(dòng)為Leaky-ReLU等激活函數(shù)計(jì)算合適的初始化因子,從而簡化了初始化過程。
import torch.nn.init as init# 假設(shè)我們使用的是Leaky-ReLU激活函數(shù)
def leaky_relu_gain(negative_slope=0.01):# 使用PyTorch的calculate_gain函數(shù)計(jì)算Leaky-ReLU的增益return init.calculate_gain('leaky_relu', negative_slope)# 計(jì)算Leaky-ReLU的增益
gain = leaky_relu_gain()
std = gain / math.sqrt(fan_in) # fan_in是輸入特征的數(shù)量# 使用計(jì)算出的增益來初始化權(quán)重
for param in model.parameters():init.normal_(param.data, mean=0.0, std=std)
4.優(yōu)化算法
除了初始化之外,為深度神經(jīng)網(wǎng)絡(luò)選擇一個(gè)合適的優(yōu)化算法也是一個(gè)重要的選擇。在深入研究這些算法之前,我們應(yīng)該定義訓(xùn)練模型的代碼。
# 根據(jù)模型路徑和名稱獲取配置文件的路徑
def _get_config_file(model_path, model_name):return os.path.join(model_path, model_name + ".config")# 根據(jù)模型路徑和名稱獲取模型文件的路徑
def _get_model_file(model_path, model_name):return os.path.join(model_path, model_name + ".tar")# 根據(jù)模型路徑和名稱獲取結(jié)果文件的路徑
def _get_result_file(model_path, model_name):return os.path.join(model_path, model_name + "_results.json")# 加載模型
def load_model(model_path, model_name, net=None):# 構(gòu)造配置文件和模型文件的路徑config_file, model_file = _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)# 確保配置文件和模型文件存在assert os.path.isfile(config_file), f"找不到配置文件\"{config_file}\"。請(qǐng)確認(rèn)路徑正確,并且模型配置已存儲(chǔ)在此位置。"assert os.path.isfile(model_file), f"找不到模型文件\"{model_file}\"。請(qǐng)確認(rèn)路徑正確,并且模型已存儲(chǔ)在此位置。"# 讀取配置文件with open(config_file, "r") as f:config_dict = json.load(f)# 如果沒有提供網(wǎng)絡(luò)結(jié)構(gòu),則根據(jù)配置文件創(chuàng)建網(wǎng)絡(luò)if net is None:act_fn_name = config_dict["act_fn"].pop("name").lower()assert act_fn_name in act_fn_by_name, f"未知的激活函數(shù)\"{act_fn_name}\"。請(qǐng)將其添加到\"act_fn_by_name\"字典中。"act_fn = act_fn_by_name[act_fn_name]()net = BaseNetwork(act_fn=act_fn, **config_dict)# 加載模型狀態(tài)net.load_state_dict(torch.load(model_file))return net# 保存模型
def save_model(model, model_path, model_name):config_dict = model.config# 創(chuàng)建模型保存路徑os.makedirs(model_path, exist_ok=True)config_file, model_file = _get_config_file(model_path, model_name), _get_model_file(model_path, model_name)# 保存配置文件和模型狀態(tài)with open(config_file, "w") as f:json.dump(config_dict, f)torch.save(model.state_dict(), model_file)# 訓(xùn)練模型
def train_model(net, model_name, optim_func, max_epochs=50, batch_size=256, overwrite=False):"""在FashionMNIST的訓(xùn)練集上訓(xùn)練模型輸入:net - BaseNetwork類型的對(duì)象model_name - (str)模型名稱,用于創(chuàng)建檢查點(diǎn)名稱max_epochs - 我們想要(最大)訓(xùn)練的周期數(shù)patience - 如果在#patience個(gè)周期內(nèi)驗(yàn)證集上的性能沒有改善,我們將提前停止訓(xùn)練batch_size - 訓(xùn)練中使用的批次大小overwrite - 確定如何處理已經(jīng)存在檢查點(diǎn)的情況。如果為True,將被覆蓋。否則,我們將跳過訓(xùn)練。"""# 省略了部分代碼...(由于代碼過長,這里省略了部分內(nèi)容,實(shí)際使用時(shí)不應(yīng)省略)# 測(cè)試模型
def test_model(net, data_loader):"""在指定的數(shù)據(jù)集上測(cè)試模型。輸入:net - 訓(xùn)練好的BaseNetwork類型的模型data_loader - 要在其上測(cè)試的數(shù)據(jù)集的DataLoader對(duì)象(驗(yàn)證或測(cè)試)"""net.eval()true_preds, count = 0., 0for imgs, labels in data_loader:imgs, labels = imgs.to(device), labels.to(device)with torch.no_grad():preds = net(imgs).argmax(dim=-1)true_preds += (preds == labels).sum().item()count += labels.shape[0]test_acc = true_preds / countreturn test_acc
首先,我們需要理解優(yōu)化器實(shí)際上是做什么的。優(yōu)化器負(fù)責(zé)根據(jù)梯度更新網(wǎng)絡(luò)的參數(shù)。因此,我們實(shí)際上實(shí)現(xiàn)了一個(gè)函數(shù) w t = f ( w t ? 1 , g t , . . . ) w^{t} = f(w^{t-1}, g^{t}, ...) wt=f(wt?1,gt,...),其中 是時(shí)間步 t 的參數(shù), g t = ? w ( t ? 1 ) L ( t ) g^{t} = \nabla_{w^{(t-1)}} \mathcal{L}^{(t)} gt=?w(t?1)?L(t)是時(shí)間步 t 的梯度。這個(gè)函數(shù)的常見額外參數(shù)是學(xué)習(xí)率,這里用 η \eta η 表示。通常,學(xué)習(xí)率可以看作是更新的“步長”。較高的學(xué)習(xí)率意味著我們更大幅度地根據(jù)梯度方向改變權(quán)重,較小的學(xué)習(xí)率意味著我們采取更短的步長。
由于大多數(shù)優(yōu)化器只在 f 的實(shí)現(xiàn)上有所不同,我們可以在PyTorch中定義一個(gè)優(yōu)化器的模板如下。我們輸入模型的參數(shù)和一個(gè)學(xué)習(xí)率。函數(shù) zero_grad
將所有參數(shù)的梯度設(shè)置為零,這是在調(diào)用 loss.backward()
之前我們必須做的。最后,step()
函數(shù)告訴優(yōu)化器根據(jù)它們的梯度更新所有權(quán)重。模板設(shè)置如下:
class OptimizerTemplate:# 初始化函數(shù),接受模型的參數(shù)和學(xué)習(xí)率def __init__(self, params, lr):self.params = list(params) # 將傳入的參數(shù)轉(zhuǎn)換為列表self.lr = lr # 學(xué)習(xí)率# 清零梯度的函數(shù)def zero_grad(self):# 遍歷所有參數(shù)for p in self.params:# 如果參數(shù)的梯度存在if p.grad is not None:p.grad.detach_() # 對(duì)于二階優(yōu)化器,這很重要p.grad.zero_() # 將梯度置為零# 應(yīng)用更新步驟的函數(shù),使用torch.no_grad()上下文管理器來禁用梯度計(jì)算@torch.no_grad()def step(self):# 遍歷所有參數(shù)for p in self.params:# 如果參數(shù)沒有梯度則跳過if p.grad is None:continueself.update_param(p) # 更新參數(shù)# 更新參數(shù)的函數(shù),需要在具體的優(yōu)化器子類中實(shí)現(xiàn)def update_param(self, p):raise NotImplementedError("Parameter update method should be implemented in optimizer-specific classes")
我們將要實(shí)現(xiàn)的第一個(gè)優(yōu)化器是標(biāo)準(zhǔn)的隨機(jī)梯度下降(SGD)。SGD使用以下公式更新參數(shù):
w ( t ) = w ( t ? 1 ) ? η ? g ( t ) \begin{split} w^{(t)} & = w^{(t-1)} - \eta \cdot g^{(t)} \end{split} w(t)?=w(t?1)?η?g(t)?
class SGD(OptimizerTemplate):# 初始化函數(shù),調(diào)用父類的初始化函數(shù)def __init__(self, params, lr):super().__init__(params, lr)# 實(shí)現(xiàn)SGD參數(shù)更新的方法def update_param(self, p):# 計(jì)算參數(shù)更新的值,這里是根據(jù)SGD的更新規(guī)則p_update = -self.lr * p.grad# 原地更新參數(shù),即直接在原參數(shù)上減去計(jì)算出的更新值# 使用add_()方法可以節(jié)省內(nèi)存,并且不會(huì)創(chuàng)建額外的計(jì)算圖p.add_(p_update)
在本文中,我們還討論了動(dòng)量概念,它通過將包括當(dāng)前梯度在內(nèi)的所有過去梯度的指數(shù)平均值來替代更新中的梯度:
m ( t ) = β 1 m ( t ? 1 ) + ( 1 ? β 1 ) ? g ( t ) w ( t ) = w ( t ? 1 ) ? η ? m ( t ) \begin{split}\begin{split} m^{(t)} & = \beta_1 m^{(t-1)} + (1 - \beta_1)\cdot g^{(t)}\\ w^{(t)} & = w^{(t-1)} - \eta \cdot m^{(t)}\\ \end{split}\end{split} m(t)w(t)?=β1?m(t?1)+(1?β1?)?g(t)=w(t?1)?η?m(t)??
class SGDMomentum(OptimizerTemplate):# 初始化函數(shù),添加動(dòng)量參數(shù)def __init__(self, params, lr, momentum=0.0):super().__init__(params, lr)self.momentum = momentum # 對(duì)應(yīng)于公式中的 beta_1# 創(chuàng)建一個(gè)字典,用于存儲(chǔ)每個(gè)參數(shù)的動(dòng)量項(xiàng) m_tself.param_momentum = {p: torch.zeros_like(p.data) for p in self.params}# 實(shí)現(xiàn)帶動(dòng)量的SGD參數(shù)更新方法def update_param(self, p):# 計(jì)算當(dāng)前參數(shù)的動(dòng)量項(xiàng),這里是指數(shù)加權(quán)平均的實(shí)現(xiàn)self.param_momentum[p] = (1 - self.momentum) * p.grad + self.momentum * self.param_momentum[p]# 計(jì)算參數(shù)更新的值,結(jié)合了學(xué)習(xí)率和動(dòng)量項(xiàng)p_update = -self.lr * self.param_momentum[p]# 原地更新參數(shù),節(jié)省內(nèi)存且不創(chuàng)建額外的計(jì)算圖p.add_(p_update)
最終,我們來到了Adam優(yōu)化器。Adam結(jié)合了動(dòng)量的概念和基于平方梯度的指數(shù)平均值的自適應(yīng)學(xué)習(xí)率,即梯度的范數(shù)。此外,我們?yōu)閯?dòng)量和自適應(yīng)學(xué)習(xí)率在最初的迭代中添加了偏差校正:
m ( t ) = β 1 m ( t ? 1 ) + ( 1 ? β 1 ) ? g ( t ) v ( t ) = β 2 v ( t ? 1 ) + ( 1 ? β 2 ) ? ( g ( t ) ) 2 m ^ ( t ) = m ( t ) 1 ? β 1 t , v ^ ( t ) = v ( t ) 1 ? β 2 t w ( t ) = w ( t ? 1 ) ? η v ^ ( t ) + ? ° m ^ ( t ) \begin{split}\begin{split} m^{(t)} & = \beta_1 m^{(t-1)} + (1 - \beta_1)\cdot g^{(t)}\\ v^{(t)} & = \beta_2 v^{(t-1)} + (1 - \beta_2)\cdot \left(g^{(t)}\right)^2\\ \hat{m}^{(t)} & = \frac{m^{(t)}}{1-\beta^{t}_1}, \hat{v}^{(t)} = \frac{v^{(t)}}{1-\beta^{t}_2}\\ w^{(t)} & = w^{(t-1)} - \frac{\eta}{\sqrt{\hat{v}^{(t)}} + \epsilon}\circ \hat{m}^{(t)}\\ \end{split}\end{split} m(t)v(t)m^(t)w(t)?=β1?m(t?1)+(1?β1?)?g(t)=β2?v(t?1)+(1?β2?)?(g(t))2=1?β1t?m(t)?,v^(t)=1?β2t?v(t)?=w(t?1)?v^(t)?+?η?°m^(t)??
Epsilon是一個(gè)非常小的常數(shù),用于提高梯度范數(shù)非常小的情況下的數(shù)值穩(wěn)定性。請(qǐng)記住,自適應(yīng)學(xué)習(xí)率并不替代學(xué)習(xí)率超參數(shù) η \eta η ,而是作為一個(gè)額外的因素,確保不同參數(shù)的梯度具有相似的范數(shù)。
class Adam(OptimizerTemplate):# 初始化函數(shù),添加了Adam優(yōu)化器所需的參數(shù)def __init__(self, params, lr, beta1=0.9, beta2=0.999, eps=1e-8):super().__init__(params, lr)self.beta1 = beta1 # 動(dòng)量超參數(shù)self.beta2 = beta2 # 二次動(dòng)量超參數(shù)self.eps = eps # 用于數(shù)值穩(wěn)定性的小常數(shù)# 用于記錄每個(gè)參數(shù)的更新次數(shù),用于偏差校正self.param_step = {p: 0 for p in self.params}# 用于存儲(chǔ)每個(gè)參數(shù)的一階動(dòng)量self.param_momentum = {p: torch.zeros_like(p.data) for p in self.params}# 用于存儲(chǔ)每個(gè)參數(shù)的二階動(dòng)量self.param_2nd_momentum = {p: torch.zeros_like(p.data) for p in self.params}# 實(shí)現(xiàn)Adam參數(shù)更新的方法def update_param(self, p):self.param_step[p] += 1 # 更新參數(shù)的更新次數(shù)# 計(jì)算一階動(dòng)量(指數(shù)加權(quán)平均的梯度)self.param_momentum[p] = (1 - self.beta1) * p.grad + self.beta1 * self.param_momentum[p]# 計(jì)算二階動(dòng)量(指數(shù)加權(quán)平均的梯度平方)self.param_2nd_momentum[p] = (1 - self.beta2) * (p.grad)**2 + self.beta2 * self.param_2nd_momentum[p]# 計(jì)算偏差校正因子bias_correction_1 = 1 - self.beta1 ** self.param_step[p]bias_correction_2 = 1 - self.beta2 ** self.param_step[p]# 計(jì)算調(diào)整后的二階動(dòng)量和一階動(dòng)量p_2nd_mom = self.param_2nd_momentum[p] / bias_correction_2p_mom = self.param_momentum[p] / bias_correction_1# 計(jì)算自適應(yīng)學(xué)習(xí)率p_lr = self.lr / (torch.sqrt(p_2nd_mom) + self.eps)# 計(jì)算參數(shù)更新值p_update = -p_lr * p_mom
4.1.優(yōu)化器比較
在實(shí)現(xiàn)了三種優(yōu)化器(SGD、帶動(dòng)量的SGD和Adam)之后,我們可以開始分析并比較它們。首先,我們測(cè)試它們?cè)趦?yōu)化FashionMNIST數(shù)據(jù)集上的神經(jīng)網(wǎng)絡(luò)方面的表現(xiàn)。我們?cè)俅问褂梦覀兊木€性網(wǎng)絡(luò),這次使用ReLU激活函數(shù)和Kaiming初始化,這是我們之前發(fā)現(xiàn)適用于基于ReLU的網(wǎng)絡(luò)的。請(qǐng)注意,該模型對(duì)于此任務(wù)來說是過度參數(shù)化的,我們可以使用更小的網(wǎng)絡(luò)(例如100,100,100)實(shí)現(xiàn)類似的性能。然而,我們的主要興趣在于優(yōu)化器能夠多好地訓(xùn)練深度神經(jīng)網(wǎng)絡(luò),因此采用了過度參數(shù)化。
base_model = BaseNetwork(act_fn=nn.ReLU(), hidden_sizes=[512,256,256,128])
kaiming_init(base_model) # 使用Kaiming初始化方法初始化模型權(quán)重
為了進(jìn)行公平比較,我們使用三種優(yōu)化器以相同的種子訓(xùn)練完全相同的模型。如果你愿意,可以自由更改超參數(shù)(然而,那樣的話,你必須自己訓(xùn)練模型)。
SGD_model = copy.deepcopy(base_model).to(device) # 創(chuàng)建模型的深拷貝并將其移動(dòng)到設(shè)備上
SGD_results = train_model(SGD_model, "FashionMNIST_SGD",lambda params: SGD(params, lr=1e-1), # 使用SGD優(yōu)化器max_epochs=40, batch_size=256) # 訓(xùn)練參數(shù)
在上述代碼中,我們首先定義了一個(gè)基礎(chǔ)模型 base_model
,它是一個(gè)具有ReLU激活函數(shù)和特定隱藏層大小的 BaseNetwork
的實(shí)例。然后,我們使用 kaiming_init
函數(shù)對(duì)這個(gè)模型的權(quán)重進(jìn)行初始化。
接下來,我們使用 copy.deepcopy
來創(chuàng)建 base_model
的一個(gè)深拷貝,以確保在訓(xùn)練過程中不會(huì)影響原始模型。我們將這個(gè)模型移動(dòng)到適當(dāng)?shù)脑O(shè)備上(例如GPU),然后使用 train_model
函數(shù)來訓(xùn)練模型。在這個(gè)例子中,我們使用學(xué)習(xí)率為0.1的SGD優(yōu)化器進(jìn)行訓(xùn)練,最大周期數(shù)設(shè)置為40,批量大小設(shè)置為256。
通過這種方式,我們可以比較不同優(yōu)化器在相同條件下的性能。類似的步驟可以用于測(cè)試帶有動(dòng)量的SGD和Adam優(yōu)化器,只需更改 train_model
函數(shù)中的優(yōu)化器參數(shù)即可。
SGDMom_model = copy.deepcopy(base_model).to(device)
SGDMom_results = train_model(SGDMom_model, "FashionMNIST_SGDMom",lambda params: SGDMomentum(params, lr=1e-1, momentum=0.9),max_epochs=40, batch_size=256)
Adam_model = copy.deepcopy(base_model).to(device)
Adam_results = train_model(Adam_model, "FashionMNIST_Adam",lambda params: Adam(params, lr=1e-3),max_epochs=40, batch_size=256)
結(jié)果是,所有優(yōu)化器在給定模型上的表現(xiàn)都相當(dāng)好。差異太小,以至于無法得出任何重大結(jié)論。然而,請(qǐng)記住,這也可以歸因于我們選擇的初始化方式。當(dāng)將初始化方式改為較差的(例如,常數(shù)初始化)時(shí),由于其自適應(yīng)學(xué)習(xí)率,Adam通常表現(xiàn)出更強(qiáng)的魯棒性。為了展示這些優(yōu)化器的特定優(yōu)勢(shì),我們將繼續(xù)觀察一些可能的損失曲面,其中動(dòng)量和自適應(yīng)學(xué)習(xí)率至關(guān)重要。
4.2.病態(tài)曲率
病態(tài)曲率 病態(tài)曲率是一種類似于峽谷的曲面,對(duì)于普通的SGD優(yōu)化特別棘手。用文字描述,病態(tài)曲率通常在一個(gè)方向上具有陡峭的梯度,中心有一個(gè)最優(yōu)解,而在第二個(gè)方向上,我們有一個(gè)更平緩的梯度通向(全局)最優(yōu)解。讓我們首先創(chuàng)建這樣一個(gè)示例曲面并對(duì)其進(jìn)行可視化:
# 定義病態(tài)曲率損失函數(shù)
def pathological_curve_loss(w1, w2):# 這是一個(gè)病態(tài)曲率的例子。還有許多其他可能的曲面,歡迎在此實(shí)驗(yàn)!x1_loss = torch.tanh(w1)**2 + 0.01 * torch.abs(w1) # w1的損失項(xiàng)x2_loss = torch.sigmoid(w2) # w2的損失項(xiàng)return x1_loss + x2_loss # 總損失是x1_loss和x2_loss的和# 定義繪制曲面的函數(shù)
def plot_curve(curve_fn, x_range=(-5,5), y_range=(-5,5), plot_3d=False, cmap=cm.viridis, title="Pathological curvature"):# 創(chuàng)建圖形fig = plt.figure()# 根據(jù)plot_3d參數(shù)選擇創(chuàng)建3D軸還是2D軸ax = plt.axes(projection='3d') if plot_3d else plt.axes()# 創(chuàng)建x和y的值范圍x = torch.arange(x_range[0], x_range[1], (x_range[1]-x_range[0])/100.)y = torch.arange(y_range[0], y_range[1], (y_range[1]-y_range[0])/100.)# 利用meshgrid生成網(wǎng)格坐標(biāo)點(diǎn)x, y = torch.meshgrid(x, y, indexing='xy')# 計(jì)算曲面的Z值,即損失函數(shù)值z = curve_fn(x, y)# 將計(jì)算得到的Z值轉(zhuǎn)換為numpy數(shù)組x, y, z = x.numpy(), y.numpy(), z.numpy()# 根據(jù)plot_3d參數(shù)繪制3D曲面圖或2D圖像if plot_3d:ax.plot_surface(x, y, z, cmap=cmap, linewidth=1, color="#000", antialiased=False)ax.set_zlabel("loss") # 設(shè)置Z軸標(biāo)簽為"loss"else:ax.imshow(z[::-1], cmap=cmap, extent=(x_range[0], x_range[1], y_range[0], y_range[1]))# 設(shè)置圖形的標(biāo)題和坐標(biāo)軸標(biāo)簽plt.title(title)ax.set_xlabel(r"$w_1$")ax.set_ylabel(r"$w_2$")plt.tight_layout() # 調(diào)整子圖布局以適應(yīng)圖形return ax# 重置Seaborn的默認(rèn)樣式
sns.reset_orig()
# 繪制3D曲面圖
_ = plot_curve(pathological_curve_loss, plot_3d=True)
plt.show() # 顯示圖形
在優(yōu)化方面,你可以將 和 想象成權(quán)重參數(shù),而曲率則代表了 和 空間上的損失曲面。請(qǐng)注意,在典型的網(wǎng)絡(luò)中,我們擁有的參數(shù)數(shù)量遠(yuǎn)遠(yuǎn)超過兩個(gè),這種曲率也可能以多維空間中出現(xiàn)。
理想情況下,我們的優(yōu)化算法會(huì)找到峽谷的中心,并專注于沿著 方向優(yōu)化參數(shù)。然而,如果我們?cè)谏郊寡鼐€遇到某點(diǎn), 方向的梯度將遠(yuǎn)大于 ?,我們可能會(huì)從一個(gè)側(cè)面跳到另一個(gè)側(cè)面。由于梯度較大,我們將不得不降低學(xué)習(xí)率,從而顯著減慢學(xué)習(xí)速度。
為了測(cè)試我們的算法,我們可以實(shí)現(xiàn)一個(gè)簡單的函數(shù),在這樣一個(gè)曲面上訓(xùn)練兩個(gè)參數(shù):
def train_curve(optimizer_func, curve_func=pathological_curve_loss, num_updates=100, init=[5, 5]):"""該函數(shù)用于在特定的損失曲面上訓(xùn)練權(quán)重參數(shù),并記錄訓(xùn)練過程。輸入:optimizer_func - 要使用的優(yōu)化器的構(gòu)造函數(shù)。應(yīng)該只接受一個(gè)參數(shù)列表。curve_func - 損失函數(shù)(例如病態(tài)曲率)。num_updates - 優(yōu)化過程中更新/步數(shù)的數(shù)量。init - 參數(shù)的初始值。必須是一個(gè)有兩個(gè)元素的列表/元組,分別代表 w_1 和 w_2。輸出:NumPy數(shù)組,形狀為 [num_updates, 3],其中 [t,:2] 是第 t 步時(shí)的參數(shù)值,[t,2] 是第 t 步的損失。"""# 將初始值轉(zhuǎn)換為可訓(xùn)練的參數(shù)weights = nn.Parameter(torch.FloatTensor(init), requires_grad=True)# 創(chuàng)建優(yōu)化器optimizer = optimizer_func([weights])# 初始化用于記錄訓(xùn)練過程中參數(shù)和損失的列表list_points = []for _ in range(num_updates):# 計(jì)算損失loss = curve_func(weights[0], weights[1])# 將當(dāng)前的參數(shù)和損失添加到記錄列表中list_points.append(torch.cat([weights.data.detach(), loss.unsqueeze(dim=0).detach()], dim=0))# 清零梯度optimizer.zero_grad()# 反向傳播計(jì)算梯度loss.backward()# 更新參數(shù)optimizer.step()# 將記錄的點(diǎn)轉(zhuǎn)換為NumPy數(shù)組并返回points = torch.stack(list_points, dim=0).numpy()return points
下一步,讓我們?cè)谇噬蠎?yīng)用不同的優(yōu)化器。注意,我們?yōu)閮?yōu)化算法設(shè)置了一個(gè)比標(biāo)準(zhǔn)神經(jīng)網(wǎng)絡(luò)更高的學(xué)習(xí)率。
這是因?yàn)槲覀冎挥袃蓚€(gè)參數(shù),而不是數(shù)萬甚至數(shù)百萬。
SGD_points = train_curve(lambda params: SGD(params, lr=10)) # 使用SGD優(yōu)化器
SGDMom_points = train_curve(lambda params: SGDMomentum(params, lr=10, momentum=0.9)) # 使用帶動(dòng)量的SGD優(yōu)化器
Adam_points = train_curve(lambda params: Adam(params, lr=1)) # 使用Adam優(yōu)化器# 為了最好地理解不同算法的工作方式,我們通過損失曲面繪制更新步驟的折線圖。
# 為了可讀性,我們將堅(jiān)持使用2D表示。# 將所有優(yōu)化器的點(diǎn)合并到一個(gè)數(shù)組中
all_points = np.concatenate([SGD_points, SGDMom_points, Adam_points], axis=0)
# 繪制損失曲面并標(biāo)記不同優(yōu)化器的路徑
ax = plot_curve(pathological_curve_loss,x_range=(-np.absolute(all_points[:, 0]).max(), np.absolute(all_points[:, 0]).max()),y_range=(all_points[:, 1].min(), all_points[:, 1].max()),plot_3d=False
)
ax.plot(SGD_points[:, 0], SGD_points[:, 1], color="red", marker="o", zorder=1, label="SGD") # SGD路徑
ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], color="blue", marker="o", zorder=2, label="SGDMom") # 帶動(dòng)量的SGD路徑
ax.plot(Adam_points[:, 0], Adam_points[:, 1], color="grey", marker="o", zorder=3, label="Adam") # Adam路徑
plt.legend() # 顯示圖例
plt.show() # 顯示圖形
這段代碼首先使用三種不同的優(yōu)化器(SGD、帶動(dòng)量的SGD和Adam)在病態(tài)曲率損失曲面上進(jìn)行訓(xùn)練,并記錄了每一步的參數(shù)值和損失。然后,它將所有優(yōu)化器的訓(xùn)練路徑合并到一個(gè)數(shù)組中,并使用plot_curve
函數(shù)繪制損失曲面的2D表示。在2D圖形上,使用不同的顏色和標(biāo)記樣式繪制了每種優(yōu)化器的路徑,并添加了圖例來標(biāo)識(shí)每種優(yōu)化器。最后,顯示了這個(gè)圖形,讓我們可以直觀地比較不同優(yōu)化器在病態(tài)曲率上的優(yōu)化過程。
我們可以清楚地看到,SGD(隨機(jī)梯度下降)無法找到優(yōu)化曲線的中心,并且由于方向上的梯度非常陡峭,它在收斂方面存在問題。相比之下,Adam和帶動(dòng)量的SGD能夠很好地收斂,因?yàn)?方向上變化的方向在不斷抵消自身。在這類曲面上,使用動(dòng)量至關(guān)重要。
4.3.陡峭的最優(yōu)值
第二種具有挑戰(zhàn)性的損失曲面是陡峭的最優(yōu)值。在這些曲面中,有一大部分區(qū)域的梯度非常小,而在最優(yōu)值周圍,我們有非常大的梯度。例如,考慮以下?lián)p失曲面:
# 定義一個(gè)二元高斯函數(shù)
def bivar_gaussian(w1, w2, x_mean=0.0, y_mean=0.0, x_sig=1.0, y_sig=1.0):norm = 1 / (2 * np.pi * x_sig * y_sig) # 高斯分布的歸一化因子x_exp = (-1 * (w1 - x_mean)**2) / (2 * x_sig**2) # w1的高斯指數(shù)部分y_exp = (-1 * (w2 - y_mean)**2) / (2 * y_sig**2) # w2的高斯指數(shù)部分return norm * torch.exp(x_exp + y_exp) # 返回二元高斯分布的值# 定義組合函數(shù),創(chuàng)建具有陡峭最優(yōu)值的損失曲面
def comb_func(w1, w2):z = -bivar_gaussian(w1, w2, x_mean=1.0, y_mean=-0.5, x_sig=0.2, y_sig=0.2)z -= bivar_gaussian(w1, w2, x_mean=-1.0, y_mean=0.5, x_sig=0.2, y_sig=0.2)z -= bivar_gaussian(w1, w2, x_mean=-0.5, y_mean=-0.8, x_sig=0.2, y_sig=0.2)return z# 使用plot_curve函數(shù)繪制具有陡峭最優(yōu)值的損失曲面
_ = plot_curve(comb_func, x_range=(-2, 2), y_range=(-2, 2), plot_3d=True, title="Steep optima"
)
plt.show()
大部分損失曲面的梯度非常小,甚至沒有梯度。然而,在最優(yōu)值附近,我們有非常陡峭的梯度。要從梯度較低的區(qū)域開始達(dá)到最小值,我們預(yù)期自適應(yīng)學(xué)習(xí)率至關(guān)重要。為了驗(yàn)證這個(gè)假設(shè),我們可以在曲面上運(yùn)行我們的三種優(yōu)化器:
# 使用train_curve函數(shù)和不同的優(yōu)化器在具有陡峭最優(yōu)值的損失曲面上進(jìn)行訓(xùn)練
SGD_points = train_curve(lambda params: SGD(params, lr=0.5), curve_func=comb_func, num_updates=1000, init=[0, 0]
)
SGDMom_points = train_curve(lambda params: SGDMomentum(params, lr=1, momentum=0.9), curve_func=comb_func, num_updates=1000, init=[0, 0]
)
Adam_points = train_curve(lambda params: Adam(params, lr=0.2), curve_func=comb_func, num_updates=1000, init=[0, 0]
)# 將不同優(yōu)化器的訓(xùn)練路徑合并到一個(gè)數(shù)組中
all_points = np.concatenate([SGD_points, SGDMom_points, Adam_points], axis=0)# 使用plot_curve函數(shù)繪制損失曲面,并在圖上繪制不同優(yōu)化器的訓(xùn)練路徑
ax = plot_curve(comb_func,x_range=(-2, 2),y_range=(-2, 2),plot_3d=False,title="Steep optima"
)
ax.plot(SGD_points[:, 0], SGD_points[:, 1], color="red", marker="o", zorder=3, label="SGD", alpha=0.7
)
ax.plot(SGDMom_points[:, 0], SGDMom_points[:, 1], color="blue", marker="o", zorder=2, label="SGDMom", alpha=0.7
)
ax.plot(Adam_points[:, 0], Adam_points[:, 1], color="grey", marker="o", zorder=1, label="Adam", alpha=0.7
)
ax.set_xlim(-2, 2) # 設(shè)置x軸的范圍
ax.set_ylim(-2, 2) # 設(shè)置y軸的范圍
plt.legend() # 顯示圖例
plt.show() # 顯示圖形
SGD最初采取的步長非常小,直到它觸及最優(yōu)值的邊界。首先到達(dá)大約(-0.75, -0.5)的點(diǎn),梯度方向發(fā)生了變化,將參數(shù)推向(0.8, 0.5),從這個(gè)點(diǎn)SGD再也無法恢復(fù)(除非經(jīng)過許多步驟)。帶動(dòng)量的SGD也有類似的問題,只不過它繼續(xù)沿著觸及最優(yōu)值的方向前進(jìn)。這個(gè)時(shí)間點(diǎn)的梯度遠(yuǎn)大于其他任何點(diǎn),以至于動(dòng)量 被它壓倒。最后,Adam能夠在最優(yōu)值處收斂,展示了自適應(yīng)學(xué)習(xí)率的重要性。
4.4. 優(yōu)化器的選擇要點(diǎn)
在看到優(yōu)化結(jié)果后,我們的結(jié)論是什么?我們應(yīng)該總是使用Adam,再也不考慮SGD了嗎?簡短的回答:不。有許多論文表明,在某些情況下,SGD(帶動(dòng)量)泛化得更好,而Adam往往傾向于過擬合[5,6]。這與尋找更寬廣的最優(yōu)值有關(guān)。
在實(shí)際應(yīng)用中,選擇哪種優(yōu)化器取決于多種因素,包括問題的具體性質(zhì)、網(wǎng)絡(luò)的架構(gòu)、訓(xùn)練數(shù)據(jù)的規(guī)模和特性等。因此,理解不同優(yōu)化器的特性并在適當(dāng)?shù)那榫持羞\(yùn)用它們是非常重要的。盡管Adam在許多情況下表現(xiàn)出色,但SGD及其變體在其他情況下可能更為合適,特別是在我們關(guān)心模型泛化能力的時(shí)候。例如,參見下圖中不同最優(yōu)值的示意圖(Keskar等人,2017年):
黑色線條代表訓(xùn)練損失曲面,而虛線紅線是測(cè)試損失。找到銳利、狹窄的最小值可能有助于發(fā)現(xiàn)最小的訓(xùn)練損失。然而,這并不意味著它也會(huì)最小化測(cè)試損失,因?yàn)橛绕涫瞧教沟淖钚≈当蛔C明具有更好的泛化能力??梢韵胂?#xff0c;由于測(cè)試數(shù)據(jù)集與訓(xùn)練集中的示例不同,其損失曲面可能會(huì)有輕微的偏移。對(duì)于銳利的最小值來說,小的變化可能會(huì)產(chǎn)生顯著的影響,而平坦的最小值通常對(duì)這種變化更加穩(wěn)健。
在下篇博文中,我們將看到某些類型的網(wǎng)絡(luò)仍然可以更好地使用SGD和學(xué)習(xí)率調(diào)度來優(yōu)化,而不是Adam。盡管如此,Adam是深度學(xué)習(xí)中最常用的優(yōu)化器,因?yàn)樗ǔ1绕渌麅?yōu)化器表現(xiàn)得更好,特別是對(duì)于深層網(wǎng)絡(luò)。
5.結(jié)論
在文中,我們討論了神經(jīng)網(wǎng)絡(luò)的初始化和優(yōu)化技術(shù)。我們看到良好的初始化必須平衡保持梯度方差和激活方差。這可以通過使用Xavier初始化實(shí)現(xiàn)對(duì)于基于tanh的網(wǎng)絡(luò),以及使用Kaiming初始化實(shí)現(xiàn)對(duì)于基于ReLU的網(wǎng)絡(luò)。在優(yōu)化方面,動(dòng)量和自適應(yīng)學(xué)習(xí)率等概念可以幫助應(yīng)對(duì)具有挑戰(zhàn)性的損失曲面,但并不能保證神經(jīng)網(wǎng)絡(luò)性能的提升。
參考文獻(xiàn)
[1] Glorot, Xavier, 和 Yoshua Bengio. “理解訓(xùn)練深度前饋神經(jīng)網(wǎng)絡(luò)的難度?!?第十三屆國際人工智能和統(tǒng)計(jì)會(huì)議論文集。2010年。鏈接
[2] He, Kaiming, 等人. “深入研究激活函數(shù):在ImageNet分類上超越人類水平的表現(xiàn)?!?2015年IEEE國際計(jì)算機(jī)視覺會(huì)議論文集。2015年。鏈接
[3] Kingma, Diederik P. & Ba, Jimmy. “Adam:一種用于隨機(jī)優(yōu)化的方法。” 第三屆國際學(xué)習(xí)表示會(huì)議(ICLR)論文集。2015年。鏈接
[4] Keskar, Nitish Shirish, 等人. “關(guān)于深度學(xué)習(xí)的大規(guī)模批量訓(xùn)練:泛化差距和尖銳最小值。” 第五屆國際學(xué)習(xí)表示會(huì)議(ICLR)論文集。2017年。鏈接
[5] Wilson, Ashia C., 等人. “自適應(yīng)梯度方法在機(jī)器學(xué)習(xí)中的邊際價(jià)值?!?神經(jīng)信息處理系統(tǒng)進(jìn)展。2017年。鏈接
[6] Ruder, Sebastian. “梯度下降優(yōu)化算法概述?!?arXiv預(yù)印本。2017年。鏈接