本節的重點在於,要對於訓練過程中神經網絡的激活,特別是向下流動的梯度有深刻的印象和理解。理解這些結構的發展歷史是很重要的,因為 RNN(循環神經網絡),作為一個通用逼近器(universal approximator),它原則上可以實現所有的算法,但不太容易用一些梯度為主的技術作優化。其不易優化的原因是一個理解重點,可以通過觀察激活和梯度在訓練中的表現來得出結論。我們也會見到很多試圖改善這種情況的變體。
起點:MLP#
來重新溫習一下上一章的 MLP 模型,也就是我們當前的起點吧:
在上一章的代碼基礎上修改,不再做硬編碼,方便後面調整。
# MLP revisited
n_embd = 10 # 字符嵌入向量的維度
n_hidden = 200 # 隱藏層中隱藏單元的數量
# vocab_size = 27, block_size = 3
g = torch.Generator().manual_seed(2147483647) # for reproducibility
C = torch.randn((vocab_size,n_embd), generator=g)
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g)
b1 = torch.randn(n_hidden, generator=g)
W2 = torch.randn((n_hidden, vocab_size), generator=g)
b2 = torch.randn(vocab_size, generator=g)
paramters = [C, W1, b1, W2, b2]
print(sum(p.nelement() for p in paramters)) # 總參數量
for p in paramters:
p.requires_grad_()
總參數量和上章一樣是 11897
神經網絡的訓練部分也做這樣不改變功能的修改:
# 和上一章作同樣的mini-batch優化
max_steps = 200000
batch_size = 32
lossi = []
for i in range(max_steps):
# mini-batch
ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
Xb, Yb = Xtr[ix], Ytr[ix] # batch X, Y
# forward pass
emb = C[Xb] # embedding
embcat = emb.view(emb.shape[0], -1) # 拼接所有嵌入向量
hpreact = embcat @ W1 + b1 # 隱藏層預處理
h = torch.tanh(hpreact) # 隱藏層
logits = h @ W2 + b2 # 輸出層
loss = F.cross_entropy(logits, Yb) # 損失函數
# backward pass
for p in parameters:
p.grad = None
loss.backward()
# update
lr = 0.1 if i < 100000 else 0.01
for p in parameters:
p.data += -lr * p.grad
# tracks stats
if i % 10000 == 0:
print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
lossi.append(loss.log10().item())
plt.plot(lossi)
注意這裡最初的損失函數高達 27,然後馬上就降到很低了,可以猜測一下原因
像一個曲棍球棍(a hockey stick)
損失函數可視化也做了一個按索引分割不同部分的損失功能:
@torch.no_grad() # 這個裝飾器可以取消跟蹤這個函數內的所有梯度
def split_loss(split):
x,y = {
'train': (Xtr, Ytr),
'val': (Xdev, Ydev),
'test': (Xte, Yte),
}[split]
# forward pass
emb = C[x] # (N, block_size, n_embd)
embcat = emb.view(emb.shape[0], -1) # 拼接為 (N, block_size * n_embd)
h = torch.tanh(embcat @ W1 + b1) # (N, n_hidden)
logits = h @ W2 + b2 # (N, vocab_size)
loss = F.cross_entropy(logits, y)
print(split, loss.item())
split_loss('train')
split_loss('val')
這個裝飾器的作用就相當於對每個 tensor 都加了個
requires_grad = False
,不會調用backward()
,所以不需要底層維護影響效率的圖表
這裡的損失為:
train 2.2318217754364014
val 2.251192569732666
# sample from the model
g = torch.Generator().manual_seed(2147483647 + 10)
for _ in range(20):
out = []
context = [0] * block_size # initialize with all ...
while True:
# 前向傳播
emb = C[torch.tensor([context])] # (1, block_size, n_embd)
h = torch.tanh(emb.view(1, -1) @ W1 + b1)
logits = h @ W2 + b2
probs = F.softmax(logits, dim=1)
# 從分布中採樣
ix = torch.multinomial(probs, num_samples=1, generator=g).item()
# 移動上下文窗口並跟蹤樣本
context = context[1:] + [ix]
out.append(ix)
# 若採樣到特殊的'.'終止符則break
if ix == 0:
break
print(''.join(itos[i] for i in out))
採樣的結果:
carlah. amorilli. kemri. rehty. sacessaeja. huteferniya. jareei. nellara. chaiir. kaleig. dham. jore. quint. sroel. alian. quinaelon. jarynix. kaeliigsat. edde. oia.
表現還不是很好,但總比 Bigram 中的強點了。
初始化修正#
首先,正如上面提到的初始損失過大,我們的神經網絡在初始化步驟有問題。
我們在初始化的時候想要的是什麼?
要預測的下一个字符有 27 種可能,我們沒有理由說其中的任何一個字符要比其他字符可能性大。所以我們期望最初的概率分布是均勻分配的(1/27)。這裡手動計算一下正確的損失是多少:
比 27 小很多
這是因為一開始神經網絡隨機分配導致字符間的概率分布差距很大導致的,可以打斷點來檢查:
logits 中應該都大概為 0,而現在這樣非常極端的分布,會導致錯誤的自信,讓損失變得很大。
由於 logits 被定義為h @ W2 + b2
,所以把這些參數變小:
W2 = torch.randn((n_hidden, vocab_size), generator=g) * 0.01
b2 = torch.randn(vocab_size, generator=g) * 0
logits 中都接近 0 了
非常接近我們想要的損失(上面提到的 3.2985)
那我們能不能把W2
設為 0 呢,這樣不就能得到最低的損失了嗎?
你不會想去把神經網絡中的權重參數設置為 exactly 0 的,這會導致對稱性破壞問題,阻礙網絡學習到有用的特徵,所以應該只會設一個很小的數字。
現在的損失還是有一些熵,這被稱為symmetry breaking。
來看看
devv.ai
對個名詞的解釋,順便推薦一下這個網站,好用的很。
去掉 break,驗證我們的優化,得到了期待的結果:
0/ 200000: 3.3221
10000/ 200000: 2.1900
...
190000/ 200000: 1.9368
train 2.123051404953003
val 2.163424015045166
現在損失函數的初始值很正常,圖像不再像一個hocky stick了,最終的性能也更好了。性能提升的原因是我們用了更多的周期去優化神經網絡,而不是用最初的幾千輪迭代用在壓縮過高的初始權重
梯度消失#
對 h 和 hpreact 進行可視化:
可以看到隱藏層中大量激活值分布在 ±1,這是由 pre-action 部分分布在過大的範圍導致的(對於絕對值較大的輸入,tanh 的輸出會飽和到 ±1)。
這其實是很不好的。可以回想一下我們在 micrograd 中實現的 tanh 及其反向傳播,tanh 永遠只會按一定比例減小梯度。在經過 tanh 單元時,我們的梯度就消失了。因為 tanh 在輸出靠近 ±1 時,處於平緩的尾部,對損失影響很小,梯度就是 0。而如果 t 是 0 的話,神經元就不活躍了(梯度原樣輸出)。
def tanh(self):
# ... forward pass code
def _backward():
self.grad += (1 - t**2) * out.grad
out._backward = _backward
return out
如果一個神經元中所有例子都是 ±1 的話,那這個神經元就完全沒有在學習,可以說是 “死神經元”。
觀察到並沒有一個神經元中完全都是白色(絕對值 > 0.99=True),神經元還是會學習的,但我們肯定還是希望在初始化上不要有這麼多預激活是 ±1 的情況。
這對於其他神經網絡中使用的非線性激活函數也是適用的,如Sigmoid這個同樣起squashing neuron(擠壓作用的)函數;對於ReLU,則是在pre-action是負的時候發生梯度消失,反向傳播時梯度歸零。除了在初始化中可能出現,如果學習率設置得過高,那麼在反向傳播過程中,某些神經元可能會接收到非常大的梯度更新。這種大的梯度更新可能會導致神經元的權重被更新到一個極端值,使得這個神經元在之後的前向傳播過程中,對於所有的輸入數據都不再產生激活(即輸出一直為 0)。這種現象被形象地稱為神經元被 “敲昏了”(knocked out),變成死神經元,就像是永久性腦損傷一樣;比較常用的ELU也有這個問題。
而Leaky ReLU則不會有這個問題,可以看到它並沒有平坦的尾部,總是會得到梯度。
權重初始化#
問題的源頭hpreact
是嵌入後乘以 w1+b1,離 0 太遠了。而我們想要的和我們之前對 logits 的期望非常相似:
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * 0.1
b1 = torch.randn(n_hidden, generator=g) * 0.01
現在的直方圖就好多了,這是因為在 pre-action 時,分布範圍減少到了 (-2,1.5)
可以看到現在不再有在 0.99 以上的神經元。
將 W1 乘的系數增大到 0.2,得到了比較滿意的效果:
-
原始:
train 2.2318217754364014
val 2.251192569732666 -
修復了 softmax 過度自信的問題:
train 2.123051404953003
val 2.163424015045166 -
修復了初始化時 tanh 層過度飽和問題:
train 2.035430431365967
val 2.1064531803131104
可以看到,儘管初始化很糟糕,但這個網絡還是學到了一定的特徵。但這只是因為這個單層 MLP 網絡很淺,優化問題很簡單,對於這些錯誤比較寬容。面對更深層的網絡時, 這些問題就會影響嚴重了。
那對於又大又深的神經網絡,我們怎麼設置這些調整縮放 scale 呢?
初始化策略#
我們來觀察一下預激活時,對於兩個高斯分布相乘後均值和標準差會發生什麼變化呢?
均值還是接近 0,因為這是對稱運算,但是標準差翻了三倍,所以這個高斯分布是在延展的。
在神經網絡中,我們希望在初始化時,每層的激活分布不要有太大的差異,以避免梯度消失或梯度爆炸問題。如果每層的激活值分布得太廣或太狹,會導致網絡在訓練時的不穩定。理想情況下,網絡在初始時應該具有良好的激活分布,這樣可以確保信息和梯度能夠有效地在網絡中傳播。
這裡對應著一個概念:內部協變量偏移(Internal Covariate Shift)
那縮放的比例設置為多少合適呢?
w = torch.randn(10, 200) * 0.3
'''
tensor(-0.0237) tensor(1.0103)
tensor(-0.0005) tensor(0.9183)
已經接近了,那如何正好是標準正態分布呢?
'''
神經網絡的權重初始化過程中,通常會將權重的初始值標準化,使得每個神經元的權重都除以其輸入連接數(fan-in)的平方根。
x = torch.randn(1000, 10)
w = torch.randn(10, 200) / 10**0.5 # 輸入元素數量是10
在這個問題上引用較多的是Delving Deep into Rectifiers: Surpassing Human-Level Performance on Image Net Classification。這篇論文在研究卷積神經網絡中,特別研究了 ReLU 和 P-ReLU 的非線性(nonlinearity)。前面說過 ReLU 會將負的激活全部歸零,相當於扔掉了一半的分布,因此你需要在神經網絡的前向傳播中對此做補償。研究發現權重初始化必須用標準差是的zero-mean Gaussian,而我們這裡使用的,這正是因為 ReLU 丟棄了一半的分布。
這一點在 PyTorch 中是集成了的,見鏈接torch.nn.init:
torch.nn.init.kaiming_normal_(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu', generator=None)
這個函數的參數意義如下:
tensor
: 要初始化的張量。a
: 正態分布的均值,默認為 0。mode
: 計算標準差的模式,可選值為 'fan_in' 和 'fan_out',默認為 'fan_in'。'fan_in' 用於輸入,'fan_out' 用於輸出。nonlinearity
: 非線性激活函數的類型,可選值為 'torch.nn.functional' 中的激活函數,默認為torch.nn.functional.relu
。
根據文章的結論,對於不同的nonlinearity激活函數,對應的增益也不同:
這裡的 ReLU 的增益對應著上面提到的
我們使用的 tanh 也需要這樣的增益,原因在於就像 ReLU 是將負的擠壓為 0,tanh 是將尾部擠壓,因此需要防止這個擠壓操作的增益,讓分布回到標準正態分布。
多年前的研究中,神經網絡在初始化問題上十分脆弱。然而現代技術創新,如殘差網絡、多種歸一化層和比隨機梯度下降更好的優化器(Adam 等),使得 “保證初始化完全正確” 相對不再那麼重要。
實踐中,我們修改先前的增益,根據公式:
W1 = torch.randn((n_embd * block_size, n_hidden), generator=g) * (5/3)/((n_embd * block_size)**0.5) # 0.2
重新訓練,得到了相近的結果。
batch norm#
批處理歸一化
影響深遠的Batch Normalization是谷歌的一個團隊在 2015 年提出的,它讓訓練深度神經網絡成為可能
文章提出的核心觀點是:你可以把隱藏層狀態(對應我們的hpreact
)直接修正為標準正態分布。
hpreact = embcat @ W1 + b1 # 隱藏層預處理
hpreact = (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) # batch normalization
這確保了即使是在深層網絡中,每個神經元的 firing rate(即激活值)在初始化階段都能保持在有利於梯度下降優化的近似高斯分布。但是在後面,我們需要讓反向傳播告訴我們這個分布應該如何變換,變得更尖銳還是更分散,對應的這會讓一些神經元變得更容易 trigger happy(激活) 還是更難被激活。
因此,我們還有一個scale and shift步驟,對標準化後的分布增益並加上偏差,來得到這一層的輸出。
# 添加參數
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
parameters = [C, W1, b1, W2, b2, bngain, bnbias]
# scale and shift
hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias # batch normalization
在初始化時,gain 和 bias 分別是 1 和 0,此時分布是我們想要的標準正態分布。因為它們都是可微的,在後面的優化過程中可以進行反向傳播。
在添加 batch norm layer 後的性能為:
train 2.0668270587921143
val 2.104844808578491
跟前面比變化不大,這是因為我們面對的是一個只有一個隱藏層的簡單例子,我們可以直接計算出能讓 hpreact 變成大致高斯分布的權重矩陣 (W1) 尺度,批處理歸一化在這裡沒做什麼。但可以想像的是,一旦在更深層的神經系統中,有很多不同類型的操作,調整權重矩陣的尺度會相當困難。這時候用在整個神經網絡的不同層次中均勻添加 batch norm layer 就容易很多了。一般習慣是在線性層(linear layer)和卷積層(convolution layer),在後面附加一個批歸一化層來控制這些激活在神經網絡每一個點的規模。
Batch Normalization 在實踐中效果很好,並且有一些正則化(Regularization)的副作用,能有效地控制激活和分布。這是由於 batch 的選擇是隨機的,可以看作是引入了額外的 “熵(entropy)”,降低了模型過擬合的風險。
Andrej Karpathy 提到批次歸一化在數學上是 “耦合的”,即這種技術使得網絡層之間的統計分布變得相互依賴。為了訓練效率,我們分成了多個 batches,但在一個 batch 的數據中,每個數據點的歸一化是依賴於整個批次的均值和方差的,這就導致了以下幾個問題:
-
批次依賴性:批次歸一化依賴於小批次數據的統計特性,這意味著模型的表現可能會受到批次大小的影響。對於小批次,均值和方差的估計可能會有很大的方差,導致訓練不穩定。
-
域變化:在不同的域(如訓練和推理)下,數據分布可能會有顯著不同,批次歸一化需要在這些不同的域下保持一致的行為。為了解決這一點,通常在訓練後使用移動平均來估計整個訓練集的均值和方差,用於推理。
-
耦合的梯度:由於批次歸一化層在反向傳播時計算梯度需要考慮整個批次的數據點,這意味著單個數據點的梯度更新不再是獨立的。這種耦合可能限制了模型優化過程中的梯度方向和幅度。
正因為這些原因,研究者一直在尋找替代的歸一化技術,比如線性歸一化(Linear Normalization)層歸一化(Layer Normalization)、組歸一化(Group Normalization)和實例歸一化(Instance Normalization)等等。
batch norm 的推理問題#
在訓練階段,每個批次的 mean 和 std 是實時計算的,用於歸一化當前批次的數據。然而,在部署模型進行推理時,我們通常是一次處理一個樣本,或者處理的批次大小與訓練時的不同,這意味著我們不能使用單個樣本的統計數據來進行批次歸一化,因為這會導致過高的方差和不穩定的預測。
為了解決這個問題,這篇論文提出的方法是讓 batch-norm 在訓練期間計算整個數據集的均值和方差的移動平均值(moving average)。這些移動平均值隨後被用來在推理時替代單個批次的統計數據。當模型被部署到生產環境中時,這些移動平均值被用來替換實時計算的批次均值和方差。這樣,無論推理時的批次大小如何,模型都使用在訓練數據上計算出的穩定統計數據。
# 在訓練結束後校準批次歸一化
with torch.no_grad():
# 將訓練集傳入
emb = C[Xtr]
embcat = emb.view(emb.shape[0], -1)
hpreact = embcat @ W1 + b1
# 計算均值和標準差
bnmean = hpreact.mean(0, keepdim=True)
bnstd = hpreact.std(0, keepdim=True)
在訓練集和驗證集上評估性能時,將動態更新的 std 和 mean 替換為我們得到的整體平均 std、mean:
# hpreact = bngain * (hpreact - hpreact.mean(0, keepdim=True)) / hpreact.std(0, keepdim=True) + bnbias
hpreact = bngain * (hpreact - bnmean) / bnstd + bnbias
現在我們就也可以傳入單個樣例做推理了。
但實際上,沒有誰會願意把 mean 和 std 的估算放在第二階段(也就是神經網絡訓練後的推理階段)。論文中有提出一個對應這個問題的想法:我們可以以移動(running)的方式在神經網絡訓練過程中估算這兩個值。
首先定義這兩個值的運行時形式並做初始化:
# BatchNorm parameters
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))
之前提到過,我們初始化了 W1 和 b1 來保證每個 preact 的分布接近標準高斯,因此均值大約是 0,標準差大約為 1。
# BatchNorm layer
# -------------------------------------------------------------
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
我們接下來要在訓練過程中更新它們,在 PyTorch,這些均值和標準差並不是基於梯度下降優化方式,我們也永遠不會去推導關於它們的梯度。
with torch.no_grad():
bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
# -------------------------------------------------------------
這裡的 0.999 和 0.001 是移動平均中的衰減系數(decay factor)的例子。衰減系數定義了移動平均中過去值和新值的相對重要性。更具體地說:
- 0.999(
momentum
)是移動平均的衰減因子,決定了之前累積的運行均值和方差保持多少。 - 0.001(等於 1 - momentum)表示新值的權重,這是每個批次的均值和方差在更新時的權重。
論文中還有這樣一步:
比如默認為,作用基本上就是防止除以 0,對應BATCHNORM1D
中的 eps 參數
其中還有一個地方是不必要的:
# Linear layer
hpreact = embcat @ W1 # + b1 去掉這裡的bias# 隱藏層預處理
# BatchNorm layer
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
Linear layer 這裡 hpreact 的 bias 現在實際上無用的,因為我們後面又對每個神經元減去了 mean,所以這些 bias 不會影響後面的計算。
所以當你使用批處理歸一化層時,如果你之前有權重層,如線性層或者卷積層,那就沒必要有 bias。這不會有什麼負面影響,只不過是訓練不會得到它的梯度,有些浪費而已。
重新整理一下網絡訓練的整體結構:
# 批次歸一化的參數和緩衝區
bngain = torch.ones((1, n_hidden))
bnbias = torch.zeros((1, n_hidden))
bnmean_running = torch.zeros((1, n_hidden))
bnstd_running = torch.ones((1, n_hidden))
max_steps = 200000
batch_size = 32
lossi = []
for i in range(max_steps):
# 構建 minibatch
ix = torch.randint(0,Xtr.shape[0],(batch_size,), generator=g)
Xb, Yb = Xtr[ix], Ytr[ix] # batch X, Y
# forward pass
emb = C[Xb] # embedding
embcat = emb.view(emb.shape[0], -1) # 拼接所有嵌入向量
# Linear layer
hpreact = embcat @ W1 # + b1 # 隱藏層預處理
# BatchNorm layer
# -------------------------------------------------------------
bnmeani = hpreact.mean(0, keepdim=True)
bnstdi = hpreact.std(0, keepdim=True)
hpreact = bngain * (hpreact - bnmeani) / bnstdi + bnbias
with torch.no_grad():
bnmean_running = 0.999 * bnmean_running + 0.001 * bnmeani
bnstd_running = 0.999 * bnstd_running + 0.001 * bnstdi
# -------------------------------------------------------------
# Non-linearity
h = torch.tanh(hpreact) # 隱藏層
logits = h @ W2 + b2 # 輸出層
loss = F.cross_entropy(logits, Yb) # 損失函數
# backward pass
for p in parameters:
p.grad = None
loss.backward()
# update
lr = 0.1 if i < 100000 else 0.01
for p in parameters:
p.data += -lr * p.grad
# tracks stats
if i % 10000 == 0:
print(f'{i:7d}/{max_steps:7d}: {loss.item():.4f}')
lossi.append(loss.log10().item())
以resnet中的結構為例:
這裡的 conv 卷積層基本上和我們前面用的線性層是一樣的,只不過卷積層是用於圖像的,所以它們有空間結構。而線性層是處理成塊的圖像數據,全連接層沒有空間概念。
基本結構和我們現前看到的一樣,都是一個 weight layer,一個 normalization layer 和 nonlinearity。
總結(省流)#
對激活,梯度以及它們在神經網絡中的統計特性的理解是非常重要的,特別是當神經網絡更大更深時。
回顧一下前面的內容:
-
初始化修正:如果我們有過度自信的錯誤預測,最後一層的激活會相當混亂,可能會以 hockey stick 的損失函數告終,解決這個問題會讓損失變好,因為你省去了浪費的訓練。
-
權重初始化:後面我們得知要控制激活的分布,我們不希望它們被壓縮到 0 或者到無窮遠,你希望神經網絡中所有東西都是均勻的,接近高斯分布。等價的問題是 “如何縮放權重矩陣和初始化過程中的偏差”,暫時可以查表來精確地定義權重和偏差。
-
batch norm:但網絡更大更深後就需要引入歸一化層了,其中最先出現的是batch normalization。基本思想是如果想要大致高斯分布,那就取均值和標準差並將數據居中
-
batch norm 的推理問題:將均值和標準差的估計放到訓練部分中,得到了 batch norm layer,它能夠非常有效地控制神經網絡中激活的統計特性。但是沒人喜歡這一層,因為它會帶來很多 bug,儘量避免它們選用其它替代方案如group normalization或layer normalization。
在使用像 Adam 這樣的高級優化器,或是殘差連接(Residual connection),訓練神經網絡基本上就是這樣的,你需要保證每一步都十分精確,需要從初始化考慮到激活和梯度,此時要訓練非常深入的網絡是不可能的。
補充 1:為什麼需要 tanh 層#
為什麼我們要把它們包括在內,還要考慮它們的增益呢?
答案很簡單,如果你只有一堆線性層,我們當然會很容易得到很好的激活,但就其表表示而言整體就只是一个線性層,不管怎麼疊加都只能得到一個線性變換。
tanh nonlinearity 就是讓我們把這個 “線性疊成的三明治” 從線性函數變成一個可以近似任意函數的神經網絡。
補充 2:學習率的設置#
Andrej Karpathy 提到了一種用來檢查學習率是否設置得當的經驗性方法,這個方法涉及到計算梯度更新與參數值本身的比例,這個比例被稱為 "ratio"。
這裡的 "ratio" 是指在單次參數更新中參數變化量與參數當前值的比率。通常,這個比率是通過計算參數更新步長(即梯度乘以學習率)與參數值的絕對值的比來得到的。比如說,如果有一個參數 和它的梯度 ,學習率為 ,那麼單次更新的 "ratio" 可以表示為:
如果這個比率在某個合理的範圍內,比如在 -3 附近,這意味著學習率設置得既不會太大,以至於使得參數更新過猛,也不會太小,以至於訓練過程緩慢無效。這個範圍確保了參數每次更新是溫和的,既能促進學習,又不至於因為過大的更新步長導致訓練不穩定。
在實際操作中,這個 "ratio" 可以用來監控和調整學習率,如果這個比率過大,可能需要減小學習率;如果這個比率過小,可能意味著可以嘗試增大学习率。这是一种启发式的方法,可以帮助研究人员和实践者调试他们的神经网络模型。