六、反向傳播算法
反向傳播(Back Propagation,簡稱BP)算法是用于訓練神經網絡的核心算法之一,它通過計算損失函數(如均方誤差或交叉熵)相對于每個權重參數的梯度,來優化神經網絡的權重。
1、前向傳播
前向傳播(Forward Propagation)把輸入數據經過各層神經元的運算并逐層向前傳輸,一直到輸出層為止。
1.1數學表達
下面是一個簡單的三層神經網絡(輸入層、隱藏層、輸出層)前向傳播的基本步驟分析。
-
輸入層到隱藏層
給定輸入 x 和權重矩陣 W1 及偏置向量 b1,隱藏層的輸出(激活值)計算如下:
將z^(1)?通過激活函數 σ進行激活:
- 隱藏層到輸出層
隱藏層的輸出 a^(1)?通過輸出層的權重矩陣 W2和偏置 b2 生成最終的輸出:
輸出層的激活值 a^(2)?是最終的預測結果:
1.2作用
前向傳播的主要作用是:
-
計算神經網絡的輸出結果,用于預測或計算損失。
-
在反向傳播中使用,通過計算損失函數相對于每個參數的梯度來優化網絡。
2、BP基礎之梯度下降算法
梯度下降算法的目標是找到使損失函數 L(θ) 最小的參數θ,其核心是沿著損失函數梯度的負方向更新參數,以逐步逼近局部或全局最優解,從而使模型更好地擬合訓練數據。
2.1數學描述
數學公式:
其中,α是學習率:
-
學習率太小,每次訓練之后的效果太小,增加時間和算力成本。
-
學習率太大,大概率會跳過最優解,進入無限的訓練和震蕩中。
-
解決的方法就是,學習率也需要隨著訓練的進行而變化。
過程:
-
初始化參數:隨機初始化模型的參數 θ ,如權重 W和偏置 b。
-
計算梯度:損失函數 L(θ)對參數 θ 的梯度 ?_θL(θ),表示損失函數在參數空間的變化率。
-
更新參數:按照梯度下降公式更新參數:θ := θ - α ?_θ L(θ),其中,α 是學習率,用于控制更新步長。
-
迭代更新:重復【計算梯度和更新參數】步驟,直到某個終止條件(如梯度接近0、不再收斂、完成迭代次數等)。
2.2傳統下降方式
根據計算梯度時數據量不同,常見的方式有:
2.2.1批量梯度下降
Batch Gradient Descent BGD
-
特點:
-
每次更新參數時,使用整個訓練集來計算梯度。
-
-
優點:
-
收斂穩定,能準確地沿著損失函數的真實梯度方向下降。
-
適用于小型數據集。
-
-
缺點:
-
對于大型數據集,計算量巨大,更新速度慢。
-
需要大量內存來存儲整個數據集。
-
例如,在訓練集中有100個樣本,迭代50輪。
那么在每一輪迭代中,都會一起使用這100個樣本,計算整個訓練集的梯度,并對模型更新。
所以總共會更新50次梯度。
因為每次迭代都會使用整個訓練集計算梯度,所以這種方法可以得到準確的梯度方向。
但如果數據集非常大,那么就導致每次迭代都很慢,計算成本就會很高。
2.2.2隨機梯度下降
Stochastic Gradient Descent, SGD
-
特點:
-
每次更新參數時,僅使用一個樣本來計算梯度。
-
-
優點:
-
更新頻率高,計算快,適合大規模數據集。
-
能夠跳出局部最小值,有助于找到全局最優解。
-
-
缺點:
-
收斂不穩定,容易震蕩,因為每個樣本的梯度可能都不完全代表整體方向。
-
需要較小的學習率來緩解震蕩。
-
例如,如果訓練集有100個樣本,迭代50輪,那么每一輪迭代,會遍歷這100個樣本,每次會計算某一個樣本的梯度,然后更新模型參數。
換句話說,100個樣本,迭代50輪,那么就會更新100*50=5000次梯度。
因為每次只用一個樣本訓練,所以迭代速度會非常快。
但更新的方向會不穩定,這也導致隨機梯度下降,可能永遠都不會收斂。
不過也因為這種震蕩屬性,使得隨機梯度下降,可以跳出局部最優解。
這在某些情況下,是非常有用的。
2.2.3小批量梯度下降
Mini-batch Gradient Descent MGBD
-
特點:
-
每次更新參數時,使用一小部分訓練集(小批量)來計算梯度。
-
-
優點:
-
在計算效率和收斂穩定性之間取得平衡。
-
能夠利用向量化加速計算,適合現代硬件(如GPU)。
-
-
缺點:
-
選擇適當的批量大小比較困難;批量太小則接近SGD,批量太大則接近批量梯度下降。
-
通常會根據硬件算力設置為32\64\128\256等2的次方。
-
例如,如果訓練集中有100個樣本,迭代50輪。
如果設置小批量的數量是20,那么在每一輪迭代中,會有5次小批量迭代。
換句話說,就是將100個樣本分成5個小批量,每個小批量20個數據,每次迭代用一個小批量。
因此,按照這樣的方式,會對梯度,進行50輪*5個小批量=250次更新。
2.3存在的問題
-
收斂速度慢:BGD和MBGD使用固定學習率,太大會導致震蕩,太小又收斂緩慢。
-
局部最小值和鞍點問題:SGD在遇到局部最小值或鞍點時容易停滯,導致模型難以達到全局最優。
-
訓練不穩定:SGD中的噪聲容易導致訓練過程中不穩定,使得訓練陷入震蕩或不收斂。
2.4優化下降方式
通過對標準的梯度下降進行改進,來提高收斂速度或穩定性。
2.4.1指數加權平均
我們平時說的平均指的是將所有數加起來除以數的個數,很單純的數學。再一個是移動平均數,指的是計算最近鄰的N個數來獲得平均數,感覺比純粹的直接全部求均值高級一點。
指數加權平均:Exponential Moving Average,簡稱EMA,是一種平滑時間序列數據的技術,它通過對過去的值賦予不同的權重來計算平均值。與簡單移動平均不同,EMA賦予最近的數據更高的權重,較遠的數據則權重較低,這樣可以更敏感地反映最新的變化趨勢。
2.4.2 Momentum
動量(Momentum)是對梯度下降的優化方法,可以更好地應對梯度變化和梯度消失問題,從而提高訓練模型的效率和穩定性。它通過引入 指數加權平均 來積累歷史梯度信息,從而在更新參數時形成“動量”,幫助優化算法更快地越過局部最優或鞍點。
特點:
-
慣性效應: 該方法加入前面梯度的累積,這種慣性使得算法沿著當前的方向繼續更新。如遇到鞍點,也不會因梯度逼近零而停滯。
-
減少震蕩: 該方法平滑了梯度更新,減少在鞍點附近的震蕩,幫助優化過程穩定向前推進。
-
加速收斂: 該方法在優化過程中持續沿著某個方向前進,能夠更快地穿越鞍點區域,避免在鞍點附近長時間停留。
總結:
-
動量項更新:利用當前梯度和歷史動量來計算新的動量項。
-
權重參數更新:利用更新后的動量項來調整權重參數。
-
梯度計算:在每個時間步計算當前的梯度,用于更新動量項和權重參數。
Momentum 算法是對梯度值的平滑調整,但是并沒有對梯度下降中的學習率進行優化。
2.4.3 AdaGrad
AdaGrad(Adaptive Gradient Algorithm)為每個參數引入獨立的學習率,它根據歷史梯度的平方和來調整這些學習率。具體來說,對于頻繁更新的參數,其學習率會逐漸減小;而對于更新頻率較低的參數,學習率會相對較大。AdaGrad避免了統一學習率的不足,更多用于處理稀疏數據和梯度變化較大的問題。
AdaGrad流程:
- 初始化
- 梯度計算
- 累積梯度的平方
- 參數更新
優點:
-
自適應學習率:由于每個參數的學習率是基于其梯度的累積平方和 G_{t,i} 來動態調整的,這意味著學習率會隨著時間步的增加而減少,對梯度較大且變化頻繁的方向非常有用,防止了梯度過大導致的震蕩。
-
適合稀疏數據:AdaGrad 在處理稀疏數據時表現很好,因為它能夠自適應地為那些較少更新的參數保持較大的學習率。
缺點:
-
學習率過度衰減:隨著時間的推移,累積的時間步梯度平方值越來越大,導致學習率逐漸接近零,模型會停止學習。
-
不適合非稀疏數據:在非稀疏數據的情況下,學習率過快衰減可能導致優化過程早期停滯。
AdaGrad是一種有效的自適應學習率算法,然而由于學習率衰減問題,我們會使用改 RMSProp 或 Adam 來替代。
2.4.4 RMSProp
雖然 AdaGrad 能夠自適應地調整學習率,但隨著訓練進行,累積梯度平方 G_t會不斷增大,導致學習率逐漸減小,最終可能變得過小,導致訓練停滯。
RMSProp(Root Mean Square Propagation)是一種自適應學習率的優化算法,在時間步中,不是簡單地累積所有梯度平方和,而是使用指數加權平均來逐步衰減過時的梯度信息。旨在解決 AdaGrad 學習率單調遞減的問題。它通過引入 指數加權平均 來累積歷史梯度的平方,從而動態調整學習率。
優點
-
適應性強:RMSProp自適應調整每個參數的學習率,對于梯度變化較大的情況非常有效,使得優化過程更加平穩。
-
適合非稀疏數據:相比于AdaGrad,RMSProp更加適合處理非稀疏數據,因為它不會讓學習率減小到幾乎為零。
-
解決過度衰減問題:通過引入指數加權平均,RMSProp避免了AdaGrad中學習率過快衰減的問題,保持了學習率的穩定性
缺點
依賴于超參數的選擇:RMSProp的效果對衰減率 \gamma 和學習率 \eta 的選擇比較敏感,需要一些調參工作。
2.4.5 Adam
Adam(Adaptive Moment Estimation)算法將動量法和RMSProp的優點結合在一起:
-
動量法:通過一階動量(即梯度的指數加權平均)來加速收斂,尤其是在有噪聲或梯度稀疏的情況下。
-
RMSProp:通過二階動量(即梯度平方的指數加權平均)來調整學習率,使得每個參數的學習率適應其梯度的變化。
Adam過程
-
初始化
-
梯度計算
-
一階動量估計(梯度的指數加權平均)
-
二階動量估計(梯度平方的指數加權平均)
-
偏差校正
優點
-
高效穩健:Adam結合了動量法和RMSProp的優勢,在處理非靜態、稀疏梯度和噪聲數據時表現出色,能夠快速穩定地收斂。
-
自適應學習率:Adam通過一階和二階動量的估計,自適應調整每個參數的學習率,避免了全局學習率設定不合適的問題。
-
適用大多數問題:Adam幾乎可以在不調整超參數的情況下應用于各種深度學習模型,表現良好。
缺點
-
超參數敏感:盡管Adam通常能很好地工作,但它對初始超參數(如β1、β2 和 η)仍然較為敏感,有時需要仔細調參。
-
過擬合風險:由于Adam會在初始階段快速收斂,可能導致模型陷入局部最優甚至過擬合。因此,有時會結合其他優化算法(如SGD)使用。
2.5總結
梯度下降算法通過不斷更新參數來最小化損失函數,是反向傳播算法中計算權重調整的基礎。在實際應用中,根據數據的規模和計算資源的情況,選擇合適的梯度下降方式(批量、隨機、小批量)及其變種(如動量法、Adam等)可以顯著提高模型訓練的效率和效果。
Adam是目前最為流行的優化算法之一,因其穩定性和高效性,廣泛應用于各種深度學習模型的訓練中。Adam結合了動量法和RMSProp的優點,能夠在不同情況下自適應調整學習率,并提供快速且穩定的收斂表現。
import torch
from sympy.abc import alpha
from torch.utils.data import TensorDataset,DataLoader
from torch import nn,optimdef test01():model = nn.Linear(10,5)x = torch.randn(10000,10)y = torch.randn(10000,5)criterion = nn.MSELoss()# momentum:動量,根據歷史梯度增加慣性# 參數值:動量系數,一般取0.9opt = optim.SGD(model.parameters(),lr=0.01,momentum=0.9)dataset = TensorDataset(x,y)# 批量梯度下降# dataloader = DataLoader(# dataset=dataset,# batch_size=len( dataset),# shuffle= True# )# 隨機梯度下降,隨機選擇一條樣本進行梯度更新# dataloader = DataLoader(# dataset=dataset,# batch_size=1,# shuffle= True# )# 小批量梯度下降dataloader = DataLoader(dataset=dataset,batch_size=64,shuffle= True)epochs = 200for epoch in range(epochs):for tx,ty in dataloader:y_pred = model(tx)loss = criterion(y_pred,ty)opt.zero_grad()loss.backward()opt.step()print(f'epoch:{epoch},loss:{loss.item()}')def test02():model = nn.Linear(10,5)x = torch.randn(1000,10)y = torch.randn(1000,5)criterion = nn.MSELoss()# Adagrad:自適應學習率優化器# 原理:歷史梯度平方和作為學習率的分母,動態調整學習率# 優點:自適應動態調整學習率# 缺點:隨著訓練時間增加,歷史梯度平方和越來越大,導致學習率越來越小,可能會停止參數更新# eps:避免學習率的分母為0,是一個非常小的數字# opt = optim.Adagrad(model.parameters(),lr=0.1,eps=1e-08)# RMSProp:自適應學習率優化器# 原理:使用指數加權平均對歷史梯度平方求和,將平方和作為分母調整學習率# 優點:緩解歷史梯度平方和快速變大,使學習率衰減更加平穩# 缺點:需要調整alpha參數,找到最優值# opt = optim.RMSprop(model.parameters(),lr=0.1,alpha=0.9,eps=1e-08)# Adam:自適應優化器# 結合了動量和RMSprop,既優化了梯度,又能動態調整學習率# 缺點是對參數設置比較敏感,需要根據實際情況進行調整# betas參數:是一個元組,第一個元素是一階動量的系數0.9,第二個元素是二階動量的系數0.999,兩個系數是經驗值opt = optim.Adam(model.parameters(),lr=0.1,betas=(0.9,0.999),eps=1e-08)for epoch in range(50):y_pred = model(x)loss = criterion(y_pred,y)opt.zero_grad()loss.backward()opt.step()print(f'loss:{loss.item()}')if __name__ == '__main__':# test01()test02()
七、過擬合和欠擬合
在訓練深層神經網絡時,由于模型參數較多,在數據量不足時很容易過擬合。而正則化技術主要就是用于防止過擬合,提升模型的泛化能力(對新數據表現良好)和魯棒性(對異常數據表現良好)。
1、概念
1.1過擬合
過擬合是指模型對訓練數據擬合能力很強并表現很好,但在測試數據上表現較差。
過擬合常見原因有:
-
數據量不足:當訓練數據較少時,模型可能會過度學習數據中的噪聲和細節。
-
模型太復雜:如果模型很復雜,也會過度學習訓練數據中的細節和噪聲。
-
正則化強度不足:如果正則化強度不足,可能會導致模型過度學習訓練數據中的細節和噪聲。
1.2欠擬合
欠擬合是由于模型學習能力不足,無法充分捕捉數據中的復雜關系。
1.3如何判斷
那如何判斷一個錯誤的結果是過擬合還是欠擬合呢?
過擬合
訓練誤差低,但驗證時誤差高。模型在訓練數據上表現很好,但在驗證數據上表現不佳,說明模型可能過度擬合了訓練數據中的噪聲或特定模式。
欠擬合
訓練誤差和測試誤差都高。模型在訓練數據和測試數據上的表現都不好,說明模型可能太簡單,無法捕捉到數據中的復雜模式。
2、解決欠擬合
欠擬合的解決思路比較直接:
-
增加模型復雜度:引入更多的參數、增加神經網絡的層數或節點數量,使模型能夠捕捉到數據中的復雜模式。
-
增加特征:通過特征工程添加更多有意義的特征,使模型能夠更好地理解數據。
-
減少正則化強度:適當減小 L1、L2 正則化強度,允許模型有更多自由度來擬合數據。
-
訓練更長時間:如果是因為訓練不足導致的欠擬合,可以增加訓練的輪數或時間.
3、解決過擬合
避免模型參數過大是防止過擬合的關鍵步驟之一。
模型的復雜度主要由權重w決定,而不是偏置b。偏置只是對模型輸出的平移,不會導致模型過度擬合數據。
怎么控制權重w,使w在比較小的范圍內?
考慮損失函數,損失函數的目的是使預測值與真實值無限接近,如果在原來的損失函數上添加一個非0的變量
其中f(w)是關于權重w的函數,f(w)>0
要使L1變小,就要使L變小的同時,也要使f(w)變小。從而控制權重w在較小的范圍內。
3.1 L2正則化
數學表達
其中:
-
L(θ) 是原始損失函數(比如均方誤差、交叉熵等)。
-
\lambda 是正則化強度,控制正則化的力度。
-
θi 是模型的第 i 個權重參數。
-
1/2Σ_{i} θi^2 是所有權重參數的平方和,稱為 L2 正則化項。
梯度更新
在 L2 正則化下,梯度更新時,不僅要考慮原始損失函數的梯度,還要考慮正則化項的影響。更新規則為:
作用
-
防止過擬合:當模型過于復雜、參數較多時,模型會傾向于記住訓練數據中的噪聲,導致過擬合。L2 正則化通過抑制參數的過大值,使得模型更加平滑,降低模型對訓練數據噪聲的敏感性。
-
限制模型復雜度:L2 正則化項強制權重參數盡量接近 0,避免模型中某些參數過大,從而限制模型的復雜度。通過引入平方和項,L2 正則化鼓勵模型的權重均勻分布,避免單個權重的值過大。
-
提高模型的泛化能力:正則化項的存在使得模型在測試集上的表現更加穩健,避免在訓練集上取得極高精度但在測試集上表現不佳。
-
平滑權重分布:L2 正則化不會將權重直接變為 0,而是將權重值縮小。這樣模型就更加平滑的擬合數據,同時保留足夠的表達能力。
3.2 L1正則化
數學表達
梯度更新
作用
-
稀疏性:L1 正則化的一個顯著特性是它會促使許多權重參數變為 零。這是因為 L1 正則化傾向于將權重絕對值縮小到零,使得模型只保留對結果最重要的特征,而將其他不相關的特征權重設為零,從而實現 特征選擇 的功能。
-
防止過擬合:通過限制權重的絕對值,L1 正則化減少了模型的復雜度,使其不容易過擬合訓練數據。相比于 L2 正則化,L1 正則化更傾向于將某些權重完全移除,而不是減小它們的值。
-
簡化模型:由于 L1 正則化會將一些權重變為零,因此模型最終會變得更加簡單,僅依賴于少數重要特征。這對于高維度數據特別有用,尤其是在特征數量遠多于樣本數量的情況下。
-
特征選擇:因為 L1 正則化會將部分權重置零,因此它天然具有特征選擇的能力,有助于自動篩選出對模型預測最重要的特征。
L1與L2對比
-
L1 正則化 更適合用于產生稀疏模型,會讓部分權重完全為零,適合做特征選擇。
-
L2 正則化 更適合平滑模型的參數,避免過大參數,但不會使權重變為零,適合處理高維特征較為密集的場景。
3.3 Dropout
Dropout 的工作流程如下:
-
在每次訓練迭代中,隨機選擇一部分神經元(通常以概率 p丟棄,比如 p=0.5)。
-
被選中的神經元在當前迭代中不參與前向傳播和反向傳播。
-
在測試階段,所有神經元都參與計算,但需要對權重進行縮放(通常乘以 1?p),以保持輸出的期望值一致。
Dropout 是一種在訓練過程中隨機丟棄部分神經元的技術。它通過減少神經元之間的依賴來防止模型過于復雜,從而避免過擬合。
import torch
from torch import nn
from torch.onnx.symbolic_opset9 import dropoutdef test01():x = torch.randint(0,10,(5,6),dtype=torch.float)dropout = nn.Dropout(p=0.5)print(x)print(dropout(x))if __name__ == '__main__':test01()
3.4數據增強
樣本數量不足(即訓練數據過少)是導致過擬合(Overfitting)的常見原因之一,可以從以下角度理解:
-
當訓練數據過少時,模型容易“記住”有限的樣本(包括噪聲和無關細節),而非學習通用的規律。
-
簡單模型更可能捕捉真實規律,但數據不足時,復雜模型會傾向于擬合訓練集中的偶然性模式(噪聲)。
-
樣本不足時,訓練集的分布可能與真實分布偏差較大,導致模型學到錯誤的規律。
-
小數據集中,個別樣本的噪聲(如標注錯誤、異常值)會被放大,模型可能將噪聲誤認為規律。
數據增強(Data Augmentation)是一種通過人工生成或修改訓練數據來增加數據集多樣性的技術,常用于解決過擬合問題。數據增強通過“模擬”更多訓練數據,迫使模型學習泛化性更強的規律,而非訓練集中的偶然性模式。其本質是一種低成本的正則化手段,尤其在數據稀缺時效果顯著。
在了解計算機如何處理圖像之前,需要先了解圖像的構成元素。
圖像是由像素點組成的,每個像素點的值范圍為: [0, 255], 像素值越大意味著較亮。比如一張 200x200 的圖像, 則是由 40000 個像素點組成, 如果每個像素點都是 0 的話, 意味著這是一張全黑的圖像。
我們看到的彩色圖一般都是多通道的圖像, 所謂多通道可以理解為圖像由多個不同的圖像層疊加而成, 例如我們看到的彩色圖像一般都是由 RGB 三個通道組成的,還有一些圖像具有 RGBA 四個通道,最后一個通道為透明通道,該值越小,則圖像越透明。
數據增強是提高模型泛化能力(魯棒性)的一種有效方法,尤其在圖像分類、目標檢測等任務中。數據增強可以模擬更多的訓練樣本,從而減少過擬合風險。數據增強通過torchvision.transforms模塊來實現。
數據增強的好處
大幅度降低數據采集和標注成本;
模型過擬合風險降低,提高模型泛化能力;
官方地址:
transforms:Transforming and augmenting images — Torchvision 0.22 documentation
transforms:
常用變換類
-
transforms.Compose:將多個變換操作組合成一個流水線。
-
transforms.ToTensor:將 PIL 圖像或 NumPy 數組轉換為 PyTorch 張量,將圖像數據從 uint8 類型 (0-255) 轉換為 float32 類型 (0.0-1.0)。
-
transforms.Normalize:對張量進行標準化。
-
transforms.Resize:調整圖像大小。
-
transforms.CenterCrop:從圖像中心裁剪指定大小的區域。
-
transforms.RandomCrop:隨機裁剪圖像。
-
transforms.RandomHorizontalFlip:隨機水平翻轉圖像。
-
transforms.RandomVerticalFlip:隨機垂直翻轉圖像。
-
transforms.RandomRotation:隨機旋轉圖像。
-
transforms.ColorJitter:隨機調整圖像的亮度、對比度、飽和度和色調。
-
transforms.RandomGrayscale:隨機將圖像轉換為灰度圖像。
-
transforms.RandomResizedCrop:隨機裁剪圖像并調整大小。
3.4.1圖片縮放
def test01():path = 'datasets/images/100.jpg'img = Image.open(path)print( img.size)transform = transforms.Compose([transforms.Resize((224,224)),transforms.ToTensor()])t_img = transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1,2,0))plt.imshow(t_img)plt.show()
3.4.2隨機裁剪
def test02():path = 'datasets/images/100.jpg'img = Image.open(path)print(img.size)transform = transforms.Compose([# 隨機裁剪transforms.RandomCrop(size=(224,224)),transforms.ToTensor()])t_img = transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.4.3隨機水平翻轉
RandomHorizontalFlip(p):隨機水平翻轉圖像,參數p表示翻轉概率(0 ≤ p
≤ 1),p=1
表示必定翻轉,p=0
表示不翻轉
def test03():path = 'datasets/images/100.jpg'img = Image.open(path)print(img.size)transform = transforms.Compose([# 隨機水平翻轉transforms.RandomHorizontalFlip(p=0.5),transforms.ToTensor()])t_img = transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.4.4調整圖片顏色
transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
brightness:
-
亮度調整的范圍。
-
可以
float
或(min, max)
元組:-
如果是
float
(如brightness=0.2
),則亮度在[max(0, 1 - 0.2), 1 + 0.2] = [0.8, 1.2]
范圍內隨機縮放。 -
如果是
(min, max)
(如brightness=(0.5, 1.5)
),則亮度在[0.5, 1.5]
范圍內隨機縮放。
-
contrast:
-
對比度調整的范圍。
-
格式與 brightness 相同。
saturation:
-
飽和度調整的范圍。
-
格式與 brightness 相同。
hue:
-
色調調整的范圍。
-
可以是一個浮點數(表示相對范圍)或一個元組 (min, max)。
-
取值范圍必須為
[-0.5, 0.5]
(因為色相在 HSV 色彩空間中是循環的,超出范圍會導致顏色異常)。
3.4.5隨機旋轉
RandomRotation用于對圖像進行隨機旋轉。
transforms.RandomRotation(
? ? degrees,?
? ? interpolation=InterpolationMode.NEAREST,?
? ? expand=False,?
? ? center=None,?
? ? fill=0
)
degrees:
-
旋轉角度的范圍,可以是一個浮點數或元組 (min_degree, max_degree)。
-
例如,degrees=30 表示旋轉角度在 [-30, 30] 之間隨機選擇。
-
例如,degrees=(30, 60) 表示旋轉角度在 [30, 60] 之間隨機選擇。
interpolation:
-
插值方法,用于旋轉圖像。
-
默認是 InterpolationMode.NEAREST(最近鄰插值)。
-
其他選項包括 InterpolationMode.BILINEAR(雙線性插值)、InterpolationMode.BICUBIC(雙三次插值)等。
expand:
-
是否擴展圖像大小以適應旋轉后的圖像。如:當需要保留完整旋轉后的圖像時(如醫學影像、文檔掃描)
-
如果為 True,旋轉后的圖像可能會比原始圖像大。
-
如果為 False,旋轉后的圖像大小與原始圖像相同。
center:
-
旋轉中心點的坐標,默認為圖像中心。
-
可以是一個元組 (x, y),表示旋轉中心的坐標。
fill:
-
旋轉后圖像邊緣的填充值。
-
可以是一個浮點數(用于灰度圖像)或一個元組(用于 RGB 圖像)。默認填充0(黑色)
def test04():path = 'datasets/images/100.jpg'img = Image.open(path)print(img.size)transform = transforms.Compose([# 隨機旋轉# degress參數:degress=30,表示在(-30,30)范圍內隨機旋轉,degree=(30,60),表示在該范圍內隨機旋轉transforms.RandomRotation(degrees=45),transforms.ToTensor()])t_img = transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.4.6圖片轉Tensor
import torch
from PIL import Image
from torchvision import transforms
import osdef test001():dir_path = os.path.dirname(__file__)file_path = os.path.join(dir_path,'img', '1.jpg')file_path = os.path.relpath(file_path)print(file_path)# 1. 讀取圖片img = Image.open(file_path)# transforms.ToTensor()用于將 PIL 圖像或 NumPy 數組轉換為 PyTorch 張量,并自動進行數值歸一化和維度調整# 將像素值從 [0, 255] 縮放到 [0.0, 1.0](浮點數)# 自動將圖像格式從 (H, W, C)(高度、寬度、通道)轉換為 PyTorch 標準的 (C, H, W)transform = transforms.ToTensor()img_tensor = transform(img)print(img_tensor)if __name__ == "__main__":test001()
3.4.7 Tensor轉圖片
def test05():t = torch.randn(3, 224, 224)transform = transforms.Compose([# 張量轉PIL圖片transforms.ToPILImage()])img = transform(t)print(img.size)img.show()
3.4.8歸一化
-
標準化:將圖像的像素值從原始范圍(如 [0, 255] 或 [0, 1])轉換為均值為 0、標準差為 1 的分布。
-
加速訓練:標準化后的數據分布更均勻,有助于加速模型訓練。
-
提高模型性能:標準化可以使模型更容易學習到數據的特征,提高模型的收斂性和穩定性。
mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])]
均值(Mean):數據集中所有圖像在每個通道上的像素值的平均值。
標準差(Std):數據集中所有圖像在每個通道上的像素值的標準差。
RGB 三個通道的均值和標準差 不是隨便定義的,而是需要根據具體的數據集進行統計計算。這些值是 ImageNet 數據集的統計結果,已成為計算機視覺任務的默認標準。
def test06():path = 'datasets/images/100.jpg'img = Image.open(path)print(img.size)transform = transforms.Compose([transforms.ToTensor(),# Normalize:標準化# mean:均值# std:標準差# 如果數據集是官方數據集,需要查看官方提供的mean和std值# 如果是自定義的數據集,可以將mean和std設置為[0.5, 0.5, 0.5],是一個經驗值# Normalize要在ToTensor()之后執行,否則會報錯transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])t_img = transform(img)print(t_img.shape)t_img = torch.permute(t_img, (1, 2, 0))plt.imshow(t_img)plt.show()
3.4.9數據增強整合
使用transforms.Compose()把要增強的操作整合到一起:
from PIL import Image
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import torch
from torchvision import transforms, datasets, utilsdef test01():# 定義數據增強和歸一化transform = transforms.Compose([transforms.RandomHorizontalFlip(), # 隨機水平翻轉transforms.RandomRotation(10), # 隨機旋轉 ±10 度transforms.RandomResizedCrop(32, scale=(0.8, 1.0)), # 隨機裁剪到 32x32,縮放比例在0.8到1.0之間transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 隨機調整亮度、對比度、飽和度、色調transforms.ToTensor(), # 轉換為 Tensortransforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 歸一化,這是一種常見的經驗設置,適用于數據范圍 [0, 1],使其映射到 [-1, 1]])# 加載 CIFAR-10 數據集,并應用數據增強trainset = datasets.CIFAR10(root="./cifar10_data", train=True, download=True, transform=transform)dataloader = DataLoader(trainset, batch_size=4, shuffle=False)# 獲取一個批次的數據images, labels = next(iter(dataloader))# 還原圖片并顯示plt.figure(figsize=(10, 5))for i in range(4):# 反歸一化:將像素值從 [-1, 1] 還原到 [0, 1]img = images[i] / 2 + 0.5# 轉換為 PIL 圖像img_pil = transforms.ToPILImage()(img)# 顯示圖片plt.subplot(1, 4, i + 1)plt.imshow(img_pil)plt.axis('off')plt.title(f'Label: {labels[i]}')plt.show()if __name__ == "__main__":test01()
八、批量標準化
1、訓練階段的批量標準化
- 計算均值和方差
- 標準化
- 縮放和平移
- 更新全局統計量
2、測試階段的批量標準化
在測試階段,由于沒有 mini-batch 數據,無法直接計算當前 batch 的均值和方差。因此,使用訓練階段通過 EMA 計算的全局統計量(均值和方差)來進行標準化。
在測試階段,使用全局統計量對輸入數據進行標準化:
然后對標準化后的數據進行縮放和平移:
為什么使用全局統計量?
一致性:
-
在測試階段,輸入數據通常是單個樣本或少量樣本,無法準確計算均值和方差。
-
使用全局統計量可以確保測試階段的行為與訓練階段一致。
穩定性:
-
全局統計量是通過訓練階段的大量 mini-batch 數據計算得到的,能夠更好地反映數據的整體分布。
-
使用全局統計量可以減少測試階段的隨機性,使模型的輸出更加穩定。
效率:
-
在測試階段,使用預先計算的全局統計量可以避免重復計算,提高效率。
3、作用
批量標準化(Batch Normalization, BN)通過以下幾個方面來提高神經網絡的訓練穩定性、加速訓練過程并減少過擬合:
3.1 緩解梯度問題
標準化處理可以防止激活值過大或過小,避免了激活函數(如 Sigmoid 或 Tanh)飽和的問題,從而緩解梯度消失或爆炸的問題。
3.2 加速訓練
由于 BN 使得每層的輸入數據分布更為穩定,因此模型可以使用更高的學習率進行訓練。這可以加快收斂速度,并減少訓練所需的時間。
3.3 減少過擬合
-
類似于正則化:雖然 BN 不是一種傳統的正則化方法,但它通過對每個批次的數據進行標準化,可以起到一定的正則化作用。它通過在訓練過程中引入了噪聲(由于批量均值和方差的估計不完全準確),這有助于提高模型的泛化能力。
-
避免對單一數據點的過度擬合:BN 強制模型在每個批次上進行標準化處理,減少了模型對單個訓練樣本的依賴。這有助于模型更好地學習到數據的整體特征,而不是對特定樣本的噪聲進行過度擬合。
4、函數
torch.nn.BatchNorm1d
是 PyTorch 中用于一維數據的批量標準化(Batch Normalization)模塊。
torch.nn.BatchNorm1d(
? ? num_features, ? ? ? ? # 輸入數據的特征維度
? ? eps=1e-05, ? ? ? ? ? # 用于數值穩定性的小常數
? ? momentum=0.1, ? ? ? ?# 用于計算全局統計量的動量
? ? affine=True, ? ? ? ? # 是否啟用可學習的縮放和平移參數
? ? track_running_stats=True, ?# 是否跟蹤全局統計量
? ? device=None, ? ? ? ? # 設備類型(如 CPU 或 GPU)
? ? dtype=None ? ? ? ? ? # 數據類型
)
參數說明:
eps:用于數值穩定性的小常數,添加到方差的分母中,防止除零錯誤。默認值:1e-05
momentum:用于計算全局統計量(均值和方差)的動量。默認值:0.1,參考本節1.4
affine:是否啟用可學習的縮放和平移參數(γ和 β)。如果 affine=True,則模塊會學習兩個參數;如果 affine=False,則不學習參數,直接輸出標準化后的值 \hat x_i。默認值:True
track_running_stats:是否跟蹤全局統計量(均值和方差)。如果 track_running_stats=True,則在訓練過程中計算并更新全局統計量,并在測試階段使用這些統計量。如果 track_running_stats=False,則不跟蹤全局統計量,每次標準化都使用當前 mini-batch 的統計量。默認值:True
import torch
from torch import nn,optim
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as pltdef build_data():x , y = make_circles(n_samples=2000,factor=0.4,noise=0.1,random_state=42)print(x[0])print(y[0:5])x = torch.tensor(x,dtype=torch.float)y = torch.tensor(y,dtype=torch.long)# plt.scatter(x[:,0],x[:,1],c=y)# plt.show()x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.3,random_state=42)return x_train,x_test,y_train,y_test# 構建網絡模型,帶批量標準化
class NetWithBN(nn.Module):def __init__(self,in_features,out_features):super().__init__()self.fc1 = nn.Linear(in_features,128)self.bn1 = nn.BatchNorm1d(128)self.relu1 = nn.ReLU()self.fc2 = nn.Linear(128,64)self.bn2 = nn.BatchNorm1d(64)self.relu2 = nn.ReLU()self.fn3 = nn.Linear(64,out_features)def forward(self,x):x = self.relu1(self.bn1(self.fc1(x)))x = self.relu2(self.bn2(self.fc2(x)))x = self.fn3(x)return x# 創建網絡模型,不使用批量標準化
class NetWithoutBN(nn.Module):def __init__(self,in_features,out_features):super().__init__()self.fc1 = nn.Linear(in_features,128)self.relu1 = nn.ReLU()self.fc2 = nn.Linear(128,64)self.relu2 = nn.ReLU()self.fn3 = nn.Linear(64,out_features)def forward(self,x):x = self.relu1(self.fc1(x))x = self.relu2(self.fc2(x))x = self.fn3(x)return xdef train(model,x_train,y_train,epochs):# 如果網絡模型中使用了dropout或批量標準化,train()默認啟動dropout或批量標準化的功能model.train()criterion = nn.CrossEntropyLoss()opt = optim.SGD(model.parameters(),lr=0.1)loss_list = []for epoch in range(epochs):y_pred = model(x_train)print(y_pred[0])print(y_train.shape)loss = criterion(y_pred,y_train)opt.zero_grad()loss.backward()opt.step()loss_list.append(loss.item())return loss_listdef eval(model,x_test,y_test,epochs):# 驗證階段會自動關閉dropout或批量標準化的參數更新model.eval()acc_list = []for epoch in range(epochs):with torch.no_grad():y_gred = model(x_test)_, pred = torch.max(y_gred,dim=1)correct = (pred == y_test).sum().item()acc = correct / len(y_test)acc_list.append((acc))return acc_listdef plot(bn_loss_list,no_bn_loss_list,bn_acc_list,no_bn_acc_list):fig = plt.figure(figsize=(12, 5))ax1 = fig.add_subplot(1, 2, 1)ax1.plot(bn_loss_list, 'b', label='BN')ax1.plot(no_bn_loss_list, 'r', label='NoBN')ax1.legend()ax2 = fig.add_subplot(1, 2, 2)ax2.plot(bn_acc_list, 'b', label='BN')ax2.plot(no_bn_acc_list, 'r', label='NoBN')ax2.legend()plt.show()if __name__ == '__main__':x_train,x_test,y_train,y_test =build_data()bn_model = NetWithBN(2,2)nobn_model = NetWithoutBN(2,2)bn_loss_list = train(bn_model,x_train,y_train,100)no_bn_loss_list = train(nobn_model,x_train,y_train,100)bn_acc_list = eval(bn_model,x_test,y_test,100)no_bn_acc_list = eval(nobn_model,x_test,y_test,100)plot(bn_loss_list,no_bn_loss_list,bn_acc_list,no_bn_acc_list)
九、模型的保存和加載
訓練一個模型通常需要大量的數據、時間和計算資源。通過保存訓練好的模型,可以滿足后續的模型部署、模型更新、遷移學習、訓練恢復等各種業務需要求。
1、標準網絡模型構建
class MyModle(nn.Module):def __init__(self, input_size, output_size):super(MyModle, self).__init__()# 創建一個全連接網絡(full connected layer)self.fc1 = nn.Linear(input_size, 128)self.fc2 = nn.Linear(128, 64)self.fc3 = nn.Linear(64, output_size)def forward(self, x):x = self.fc1(x)x = self.fc2(x)output = self.fc3(x)return output# 創建模型實例
model = MyModel(input_size=10, output_size=2)
# 輸入數據
x = torch.randn(5, 10)
# 調用模型
output = model(x)
forward 方法是 PyTorch 中 nn.Module 類的必須實現的方法。它是定義神經網絡前向傳播邏輯的地方,決定了數據如何通過網絡層傳遞并生成輸出。同時forward 方法定義了計算圖,PyTorch 會根據這個計算圖自動計算梯度并更新參數。
2、序列化模型對象
模型序列化對象的保存和加載:
模型保存:
torch.save(obj, f, pickle_module=pickle, pickle_protocol=DEFAULT_PROTOCOL, _use_new_zipfile_serialization=True)
參數說明:
-
obj:要保存的對象,可以是模型、張量、字典等。
-
f:保存文件的路徑或文件對象。可以是字符串(文件路徑)或文件描述符。
-
pickle_module:用于序列化的模塊,默認是 Python 的 pickle 模塊。
-
pickle_protocol:pickle 模塊的協議版本,默認是 DEFAULT_PROTOCOL(通常是最高版本)。
模型加載:
torch.load(f, map_location=None, pickle_module=pickle, **pickle_load_args)
參數說明:
-
f:文件路徑或文件對象。可以是字符串(文件路徑)或文件描述符。
-
map_location:指定加載對象的設備位置(如 CPU 或 GPU)。默認是 None,表示保持原始設備位置。例如:map_location=torch.device('cpu') 將對象加載到 CPU。
-
pickle_module:用于反序列化的模塊,默認是 Python 的 pickle 模塊。
-
pickle_load_args:傳遞給 pickle_module.load() 的額外參數。
3、保存模型參數
這種形式更常用,只需要保存權重、偏置、準確率等相關參數,都可以在加載后打印觀察!
import torch
from torch import nnclass MyNet(nn.Module):def __init__(self):super().__init__()self.fc1 = nn.Linear(10,64)self.fc2 = nn.Linear(64,5)def forward(self,x):x = self.fc1(x)x = self.fc2(x)return x# 保存模型
def test01():model = MyNet()print( model)torch.save(model,'./model/fcnn_model.pt')# 加載模型
# 完整的模型
def test02():model = torch.load('./model/fcnn_model.pt')print( model)# 保存模型參數
def test03():model = MyNet()state_dict = model.state_dict()torch.save(state_dict,'./model/fcnn_state.pt')# 加載模型參數
# 如果保存的是模型參數,加載的是字典,內容是模型參數,并不是完整的模型
# 需要事先初始化模型,然后把模型參數導入到模型中
def test04():model = MyNet()state_dict = torch.load('./model/fcnn_state.pt')model.load_state_dict(state_dict)if __name__ == '__main__':# test01()test02()
十、項目實戰
1.使用全連接網絡訓練和驗證MNIST數據集
# 使用全連接網絡訓練和預測MINIST數據集
# 1.數據準備:通過數據加載器加載官方MINIST數據集
# 2.構建網絡結構
# 3.實現訓練方法:使用交叉熵損失函數、Adam優化器
# 4.實現驗證方法
# 5.通過測試圖片進行預測import torch
from torch import nn,optim
from torchvision import datasets,transforms
from torch.utils.data import DataLoader
from PIL import Imagedef build_data():transform = transforms.Compose([transforms.Resize((28, 28)), # 正確寫法:調整為28x28transforms.ToTensor(),])# 訓練數據集train_dataset = datasets.MNIST(root = './datasets',train = True,download=True,transform=transform,)# 驗證數據集eval_dataset = datasets.MNIST(root = './datasets',train = False,download=True,transform=transform,)# 訓練數據加載器train_dataloader = DataLoader(dataset=train_dataset,batch_size=64,shuffle=True,)# 驗證數據加載器eval_dataloader = DataLoader(dataset=eval_dataset,batch_size=64,shuffle=True,)return train_dataloader,eval_dataloader# 構建網絡架構
class MNISTNet(nn.Module):def __init__(self,in_fearures,out_featuers):super().__init__()self.fc1 = nn.Linear(in_fearures,128)self.bn1 = nn.BatchNorm1d(128)self.relu1 = nn.ReLU()self.fc2 = nn.Linear(128,64)self.bn2 = nn.BatchNorm1d(64)self.relu2 = nn.ReLU()self.fn3 = nn.Linear(64,out_featuers)def forward(self,x):x = x.view(-1,1*28*28)x = self.relu1(self.bn1(self.fc1(x)))x = self.relu2(self.bn2(self.fc2(x)))x = self.fn3(x)return xdef train(model,train_dataloader,lr,epochs):model.train()criterion = nn.CrossEntropyLoss()opt = optim.Adam(model.parameters(),lr=lr,betas=(0.9,0.999),eps=1e-08,weight_decay=0.001)for epoch in range(epochs):correct = 0for tx,ty in train_dataloader:y_pred = model(tx)loss = criterion(y_pred,ty)opt.zero_grad()loss.backward()opt.step()_,pred = torch.max(y_pred.data,dim=1)correct += (pred==ty).sum().item()acc = correct / len(train_dataloader.dataset)print(f'epoch:{epoch},loss:{loss.item():.4f},acc:{acc:.4f}')def eval(model,eval_dataloader):model.eval()criterion = nn.CrossEntropyLoss()correct = 0for vx,vy in eval_dataloader:with torch.no_grad():y_pred = model(vx)loss = criterion(y_pred,vy)_,pred = torch.max(y_pred.data,dim=1)correct += (pred==vy).sum().item()acc = correct / len(eval_dataloader.dataset)print(f'loss:{loss.item()},acc:{acc}')def save_model(model,path):torch.save(model.state_dict(),path)def load_model(path):model = MNISTNet(1 * 28 * 28,10)model.load_state_dict(torch.load(path))return modeldef predict(test_path,model_path):transform = transforms.Compose([transforms.Resize((28,28)),transforms.ToTensor(),])img = Image.open(test_path).convert('L')t_img = transform(img).unsqueeze(0)model = load_model(model_path)model.eval()with torch.no_grad():y_pred = model(t_img)_,pred = torch.max(y_pred.data,dim=1)print(f'預測分類:{pred.item()}')if __name__ == '__main__':# train_dataloader,val_dataloader = build_data()# model = MNISTNet(1 * 28 * 28,10)# train(model,train_dataloader,lr=0.01,epochs=20)# eval(model,val_dataloader)# save_model(model,'./model/mnist_model.pt')predict('./datasets/images/8.png', './model/mnist_model.pt')
2.使用全連接網絡訓練和驗證CIFAR10數據集
import torch
from torch import nn,optim
from torchvision import datasets,transforms
from torch.utils.data import DataLoaderdef build_data():# 數據轉換transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))])# 數據準備# 訓練數據集train_dataset = datasets.CIFAR10(root='./datasets',train=True,transform=transform,download=True)# 驗證數據集eval_dataset = datasets.CIFAR10(root='./datasets',train=False,transform=transform,download=True)# 訓練集數據加載器train_loader = DataLoader(dataset=train_dataset,batch_size=128,shuffle=True)# 驗證集數據加載器eval_loader = DataLoader(dataset=eval_dataset,batch_size=256,shuffle=False)return train_loader,eval_loader# 定義網絡結構
class CIFAR10Net(nn.Module):def __init__(self,in_features,out_features):super().__init__()self.fc1 = nn.Linear(in_features,2048)self.bn1 = nn.BatchNorm1d(2048)self.dropout1 = nn.Dropout(0.3)self.fc2 = nn.Linear(2048,1024)self.bn2 = nn.BatchNorm1d(1024)self.dropout2 = nn.Dropout(0.3)self.fc3 = nn.Linear(1024,512)self.bn3 = nn.BatchNorm1d(512)self.fc4 = nn.Linear(512,256)self.bn4 = nn.BatchNorm1d(256)self.fc5 = nn.Linear(256,out_features)self.relu = nn.ReLU()def forward(self,x):x = x.view(-1,32*32*3)x = self.dropout1(self.bn1(self.fc1(x)))x = self.relu(x)x = self.dropout2(self.bn2(self.fc2(x)))x = self.relu(x)x = self.bn3(self.fc3(x))x = self.relu(x)x = self.relu(self.bn4(self.fc4(x)))x = self.fc5(x)return xdef train(model,train_loader,lr,epochs):model.train()criterion = nn.CrossEntropyLoss()opt = optim.AdamW(model.parameters(),lr=lr,weight_decay=0.01)device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')model = model.to(device)for epoch in range(epochs):correct = 0for tx,ty in train_loader:tx,ty = tx.to(device),ty.to(device)y_pred = model(tx)loss = criterion(y_pred,ty)opt.zero_grad()loss.backward()opt.step()_,pred = torch.max(y_pred.data,dim=1)correct += (pred==ty).sum().item()acc = correct / len(train_loader.dataset)print(f'epoch:{epoch},loss:{loss.item():.4f},acc:{acc:.4f}')def eval(model,eval_loader):model.eval()criterion = nn.CrossEntropyLoss()device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')model = model.to(device)eval_loss = 0correct = 0with torch.no_grad():for vx,vy in eval_loader:vx,vy = vx.to(device),vy.to(device)y_pred = model(vx)eval_loss += criterion(y_pred,vy)_,pred = torch.max(y_pred.data,dim=1)correct += (pred==vy).sum().item()eval_loss /= len(eval_loader.dataset)acc = 100*correct / len(eval_loader.dataset)print(f'loss:{eval_loss.item()},acc:{acc}')if __name__ == '__main__':train_loader,val_loader = build_data()model = CIFAR10Net(32*32*3,10)train(model,train_loader,lr=0.001,epochs=60)eval(model,val_loader)