此分類用於記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:
- 原課程視頻鏈接:[雙語字幕]吳恩達深度學習deeplearning.ai
- github課程資料,含課件與筆記:吳恩達深度學習教學資料
- 課程配套練習(中英)與答案:吳恩達深度學習課後習題與答案
本篇為第二課第二週的課程習題和代碼實踐部分筆記。
1.理論習題
【中英】【吳恩達課後測驗】Course 2 - 改善深層神經網絡 - 第二週測驗
還是先上這位博主鏈接,然後這裏我們挑出幾道來展開一下。
1.1 符號規範
來看這道題:
當輸入從第八個mini-batch的第七個的例子的時候,你會用哪種符號表示第三層的激活?
答案:\(a^{[3]\{8\}(7)}\)
隨着幾周的不斷學習,也出現了越來越多的符號,我們在這裏梳理一下:
| 符號 | 名稱 | 含義 / 表示對象 | 備註説明 |
|---|---|---|---|
| \(x^{(i)}\) | 第 \(i\) 個樣本輸入 | 單個樣本的輸入特徵列向量 | \(n_x\):輸入層神經元個數 |
| \(y^{(i)}\) | 第 \(i\) 個樣本標籤 | 單個樣本的輸出(標量或向量) | 監督學習中的真實值 |
| \(X\) | 所有樣本輸入矩陣 | 把所有 \(x^{(i)}\) 橫向拼接 | \(m\):樣本總數 |
| \(Y\) | 所有樣本標籤矩陣 | 把所有 \(y^{(i)}\) 橫向拼接 | 通常 \(n_y=1\) |
| \(X^{\{t\}}\) | 第 t 個 mini-batch 的輸入矩陣 | 把該批次內的 \(x^{(i)}\) 橫向拼接 | - |
| \(W^{[l]}\) | 第 \(l\) 層的權重矩陣 | 當前層與上一層之間的連接權重 | 由網絡結構決定 |
| \(b^{[l]}\) | 第 \(l\) 層的偏置向量 | 當前層的偏置項 | 廣播到所有樣本 |
| \(z^{[l]}\) | 第 \(l\) 層的線性部分 | \(z^{[l]} = W^{[l]} a^{[l-1]} + b^{[l]}\) | 激活函數的輸入 |
| \(a^{[l]}\) | 第 \(l\) 層的激活值 | \(a^{[l]} = g^{[l]}(z^{[l]})\) | 上層的輸入 |
| \(g^{[l]}\) | 第 \(l\) 層激活函數 | 如 ReLU、Sigmoid、Tanh 等 | 與任務有關 |
| \(\hat{y}^{(i)}\) | 第 \(i\) 個樣本的預測 | 模型的輸出結果 | 預測值 |
| \(\hat{Y}\) | 所有樣本的預測矩陣 | 所有 \(\hat{y}^{(i)}\) 拼接 | - |
| \(J\) | 成本函數 | 衡量預測與真實標籤的差距 | 通常為交叉熵或均方誤差 |
| \(\alpha\) | 學習率 | 控制參數更新步長 | 越大越快但不穩定 |
| \(v^{[l]}\) | 動量項 | EMA 平滑後的梯度方向 | Momentum / Adam 使用 |
| \(s^{[l]}\) 或 \(S^{[l]}\) | 平方梯度累積項 | EMA 平滑後的梯度平方 | RMSprop / Adam 使用 |
| \(t\) | 當前迭代次數(步數) | 優化算法中的時間步 | Adam 等算法中使用 |
| \(m\) | 樣本總數 | - | 整個訓練集大小 |
| \(m_{mini}\) | mini-batch 樣本數 | - | 小批量訓練用 |
| \(a^{[l]\{t\}(i)}\) | 特定樣本與批次的激活 | 第 \(t\) 個 mini-batch,第 \(i\) 個樣本,第 \(l\) 層激活 | 完整標記形式,精確到樣本與批次 |
看起來有些繁雜,但實際上在這麼長時間的使用裏,我們已經記憶了大部分了。看一遍下來查漏補缺就好了。
唯一要強調的一個是\(X^{\{t\}}\) 中 \(\{t\}\) 代表的是Mini-batch 梯度下降中的第 \(t\) 個批次,\(X^{\{t\}}\) 是第 \(t\) 個批次的輸入。
這是本週裏隨着Mini-batch 梯度下降出現而引入的新規範,但是在理論部分並沒有實際用到,就放在了這裏。
1.2 EMA 平滑係數辨析
看看題面:
您在倫敦温度數據集上使用指數加權平均值, 您可以使用以下公式來追蹤温度:\(v_t = βv_{t-1} +(1 - β)θ_t\) 下面的紅線使用的是 \(β= 0.9\) 來計算的。 當你改變 \(β\) 時,你的紅色曲線會怎樣變化?
答案: 增加 \(β\) 會使紅線稍微向右移動;減少 \(β\) 會在紅線內產生更多的振盪。
回憶一下我們在指數加權平均部分對平滑係數的概念:它代表我們對“歷史印象”的依賴程度。
- 平滑係數越大,平均值越注重“以前”的數據。
- 平滑係數越小,平均值越看重“今天”的數據。
實際上,EMA不同於簡單平均的點就在於它能抹平短期波動的同時反應長期變化的方向。它反應的更像是一種“一段時間裏的趨勢”。
現在再來分析一下兩個正確答案:
- 增加 \(β\) 會使紅線稍微向右移動: 我們分析一下“右移”是什麼意思,是不是當紅線右移時,每一個點都會右移?也就是:今天的均值變成了明天的均值(時間滯後,並非真的平移)。
換句話説: 平滑後的曲線(紅線)對真實數據(藍線)的反應變得更滯後了。
再展開點:當 \(\beta\) 變大時,我們更依賴過去的歷史值,那是不是對“今天”變化的敏感度就會降低?因此,紅線的趨勢變化更平緩,更“跟不上”藍線(當天),看起來就像整體向右延遲了一點。 - 減少 \(β\) 會在紅線內產生更多的振盪:其實是一個道理,我們再換一種説法來理解。
我們之前提到等效天數的概念,減少 \(β\) 會減少等效天數,相當於用更短的窗口捕捉趨勢。那對“今天”變化的敏感度是不是就會增加?自然會更加“震盪”,因為反應的“趨勢”更短期了,自然會趨勢裏的每一天更敏感。
如果還不太明白,我們再來看一道同類型的題:
已知圖像是由梯度下降產生的。那哪條曲線對應哪種下面算法?
A.普通的下降 B.具有動量梯度下降(β= 0.5) C.動量梯度下降(β= 0.9)
答案:(1)是梯度下降。 (2)是動量梯度下降(β值比較小)。 (3)是動量梯度下降(β比較大)
結合上面那到實際題,我們再來看看這到理論的題。
我們已知,Momntum 通過對梯度應用EMA來緩解振盪現象。
説簡單點:就是對梯度取加權平均值應用更新,不過度依賴本次傳播的計算的梯度。
這裏的 ”當次傳播“”是不是就相當於“當天氣温”?
而 \(β\) 增加就相當於對以前越看重,那是不是反應的“趨勢”就越長期?自然,多次上下抵消後縱向的波動就更小?
答案很容易就可以得到,重點是理解EMA的作用原理。
2.代碼實踐
【中文】【吳恩達課後編程作業】Course 2 - 改善深層神經網絡
還是先擺鏈接,對於本週的內容,這位博主依然只使用numpy庫來手動構建各種優化算法。
我們也還是用我們的Pytorch框架把新的優化應用在貓狗二分類數據集來看看這幾種優化算法的實際效果。
2.1 小批次,批量和隨機梯度下降法
還是先回憶一下我們之前更新完的模型:
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(128 * 128 * 3, 1024)
self.hidden2 = nn.Linear(1024, 512)
self.hidden3 = nn.Linear(512, 128)
self.hidden4 = nn.Linear(128, 32)
self.hidden5 = nn.Linear(32, 8)
self.hidden6 = nn.Linear(8, 3)
self.relu = nn.ReLU()
# 輸出層
self.output = nn.Linear(3, 1)
self.sigmoid = nn.Sigmoid()
# Xavier初始化輸出層
init.xavier_uniform_(self.output.weight)
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.hidden1(x))
x = self.relu(self.hidden2(x))
x = self.relu(self.hidden3(x))
x = self.relu(self.hidden4(x))
x = self.relu(self.hidden5(x))
x = self.relu(self.hidden6(x))
x = self.sigmoid(self.output(x))
return x
optimizer = optim.SGD(model.parameters(), lr=0.01,weight_decay=0.01)
上次實踐中,我們已經實現了權重初始化和正則化。
本身實踐中,我們並不會再改動模型結構,而更側重在同一結構下使用各種優化的效果。
首先,先回憶一下小批次,批量和隨機梯度下降法的概念,它們的不同只針對批次大小。
- 隨機梯度下降法SGD:每次迭代只使用一個樣本,一個epoch中,有幾個樣本,就進行幾次傳播。
- 批量梯度下降法GD:每次迭代使用全部訓練樣本,一個epoch中,只進行一次傳播。
- 小批次梯度下降法 Mini-GD:人為規定批次大小,一個epoch中,幾個批次可以覆蓋全部訓練樣本,就進行幾次傳播。
現在來展開幾個小問題:
(1)PyTorch 封裝三種梯度下降法的邏輯
首先,你可能發現了這樣一個問題,我們之前不是一直使用的小批次下降法嗎?
為什麼代碼裏是 SGD?
optimizer = optim.SGD(model.parameters(), lr=0.01,weight_decay=0.01)
關於這個問題,實際上,PyTorch是用SGD整個代指了三種梯度下降法。
因為它們的不同只針對批次大小,更新參數的邏輯是不變的。
因此,這裏的SGD實際上指的參數更新方式。
再想想,我們想應用這三種裏的不同方式,是不是完全不必要封裝三種方法?
因為它們的不同只有一個:批次大小。
所以,PyTorch 把劃分不同批次大小封裝在了數據集劃分模塊。
這部分內容在我們第一次用PyTorch設置整體框架這裏:神經網絡基礎 課後習題和代碼實踐
直接挑出來,就是這裏:
# 1.加載數據集
dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)
# 2.劃分數據集大小
train_size = int(0.8 * len(dataset))
val_size = int(0.1 * len(dataset))
test_size = len(dataset) - train_size - val_size
train_dataset, val_dataset, test_dataset = random_split(dataset, [train_size, val_size, test_size])
# 3.最後按大小設置數據迭代器
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False)
這是我們對整個數據集的設置,而哪一部分和我們的三種梯度下降法相關?
就是這句:
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
這裏就是我們的訓練集加載器。
再核心一點,哪個參數和我們的三種梯度下降法相關?
就是這個:
batch_size = 32
不難看出這個參數的意思就是批次大小,現在我們就是在以每批次使用32個樣本進行訓練和傳播。
同理,我們想用SGD,那就是:
batch_size = 1
想用GD,那就是:
batch_size = train_size
知道了如何在PyTorch中使用不同批次大小梯度下降後,我們就來看看效果。
(2)三種梯度下降法的實際效果
還是把完整代碼放在最後,我們來看看實際運行的效果。
要提前説明的是,因為硬件原因和讓對比圖更直觀,我只把大批次設置為了256,大家理解原理就好,有興趣可以自行嘗試更大的批次效果。
我們來看看結果:
注意,這裏的橫軸單位是一個batch的迭代,不是一個epoch,這樣我們才能看到批次大小的直接影響。
就像我們理論裏説的一樣:批次越小,波動性越大,反過來也是一樣的。
這是因為一次迭代的參數更新只依賴該次梯度大小,而批次樣本越少,批次間的差別就可能更大。
同時,批次越小,一批次訓練的耗時就越少,這很直觀,更少的數據量自然帶來更快的計算。
再完善一點,你會發現大批次的曲線很短,這是因為它幾次迭代就完成了所有樣本的訓練,還是我們在理論裏提到的:批次越大,一個epoch中的迭代次數就越少。
這就是批次大小帶來的影響。
批次越大,損失下降就更平穩。
但就像我最開始説的,當批次大小過大時,硬件就可能支持不了計算量。如何在成本能支持的批次量下實現更好的效果,就是我們後面瞭解的幾種優化算法。
2.2 Momentum,RMSprop和Adam
不同於上面的只改變批次大小,這幾種算法更改了參數更新的邏輯。因此也有不同的方法。
而這些不同的優化算法都被封裝在優化器模塊裏。
現在我們固定批次大小為16,讓波動大一些,來更好的看看優化效果。
來看看這幾種算法的應用和對比。
(1)Pytorch中幾種優化算法的應用
- 首先,這是原本的普通梯度下降法:
optimizer = optim.SGD(model.parameters(), lr=lr,weight_decay=0.01)
- 而這就是動量梯度下降法:
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=0.01)
你會發現它只是在SGD裏增加了一個參數momentum=0.9,這叫動量項,0.9也就是平滑係數。
想一想,如果不使用這個參數,那在普通的SGD裏,這個參數的默認值是不是就是0,代表只依賴當次梯度。
3. 再看看RMSprop:
optimizer = optim.RMSprop(model.parameters(), lr=lr, alpha=0.99, weight_decay=0.01)
首先它更改了方法名,而參數裏的alpha=0.99,就代表梯度平方EMA的平滑係數,它的默認值就是0.99,可以省略。
4. 最後就是Adam:
optimizer = optim.Adam(model.parameters(),lr=lr,betas=(0.9,0.999),weight_decay=0.01)
betas就是設置一二階矩平滑係數的參數,這裏就是默認值,可以省略。
(2)應用結果對比
進行多次實驗,來看一下其中一次的結果:
我們來看一下,首先波動性上 :$$RMSprop>SGD>Momentum>Adam$$
我們反過來,從小到大分析:
- Adam:
作為 Momentum + RMSprop 的集成者,Adam 同時考慮了 梯度的一階矩(方向) 和 二階矩(幅度)。
因此,Adam 的損失曲線在訓練中表現為 波動最小且收斂較快。 - Momentum:
Momentum 僅平滑梯度方向,沒有幅度自適應機制。
因此,它在方向震盪上較 SGD 有明顯優勢,但梯度幅度差異仍會導致一定波動。 - SGD(普通隨機梯度下降):
- 每個 batch 都直接按當前梯度更新。
- 沒有方向平滑,也沒有步長自適應。
因此,你會發現它的波動性從始至終都相對較大,且收斂較慢。
- RMSprop:
作為一個新提出的優化算法,為什麼多次實驗中,他的平均波動都比SGD還大?
我們來看看圖像,你會發現,RMSprop在初期的波動非常大。
因為它只使用二階矩,在小批次(噪聲平均更大)和沒有方向信息(一階矩)的前提下,可能會在噪聲較大時更為敏感,就像對着一個噪聲方向猛踩油門,下一次看到對的又飆回來。
而此時我們還沒有很多批次來”平均“,就會讓其在初期較大震盪。
但你會發現,適應初期後,它的收斂就會比SGD快很多。
最後還要強調一點,這種波動排序並不絕對,訓練本身就存在一定的隨機性,有時你甚至能看到SGD的效果比Adam好。
現在我們把範圍擴大到epoch再進行幾次實驗,選擇其中一次來看看損失下降效果:
可以發現,幾種優化算法的性能確實都高於SGD。
你會發現理應最好的Adam好像”卡住了“,這也是訓練的隨機性導致的,可能下一輪它就會跳出來,可能它會一直卡在這裏。
總之,我們可以從平均收斂效果上看出幾種優化算法的優劣,而這種優劣會隨着數據量的增加而更加明顯,但要實現好的擬合效果一定是各部分協作的效果,我們不可能説只憑優化部分來極大的增加擬合效果。
對於現在而言,一般在優化算法部分,Adam就是我們的默認選擇。
3.附錄
3.1 對比不同批次大小
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn import init
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
import numpy as np
import time
transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)
train_size = int(0.8 * len(dataset))
val_size = int(0.1 * len(dataset))
test_size = len(dataset) - train_size - val_size
train_dataset, _, _ = random_split(dataset, [train_size, val_size, test_size])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(128 * 128 * 3, 1024)
self.hidden2 = nn.Linear(1024, 512)
self.hidden3 = nn.Linear(512, 128)
self.hidden4 = nn.Linear(128, 32)
self.hidden5 = nn.Linear(32, 8)
self.hidden6 = nn.Linear(8, 3)
self.relu = nn.ReLU()
self.output = nn.Linear(3, 1)
self.sigmoid = nn.Sigmoid()
init.xavier_uniform_(self.output.weight)
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.hidden1(x))
x = self.relu(self.hidden2(x))
x = self.relu(self.hidden3(x))
x = self.relu(self.hidden4(x))
x = self.relu(self.hidden5(x))
x = self.relu(self.hidden6(x))
x = self.sigmoid(self.output(x))
return x
def train_and_record(batch_size, epochs=3):
loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
model = NeuralNetwork().to(device)
criterion = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=0.01)
batch_losses = []
start_time = time.time()
for epoch in range(epochs):
model.train()
for images, labels in loader:
images, labels = images.to(device), labels.to(device).float().unsqueeze(1)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
batch_losses.append(loss.item())
avg_time = (time.time() - start_time) / (epochs * len(loader))
return batch_losses, avg_time
batch_sizes = [1, 32, 256]
results = {}
times = {}
print("開始訓練並記錄每個 batch 的損失...")
for bs in batch_sizes:
print(f"\n訓練 batch_size={bs} ...")
losses, avg_t = train_and_record(bs, epochs=3)
results[bs] = losses
times[bs] = avg_t
print(f"batch_size={bs} 平均每 batch 時間: {avg_t:.4f} 秒")
max_points = 100 #統一可視化的迭代步數
aligned_losses = {}
for bs, losses in results.items():
if len(losses) > max_points:
idx = np.linspace(0, len(losses)-1, max_points).astype(int)
aligned_losses[bs] = [losses[i] for i in idx]
else:
aligned_losses[bs] = losses
def smooth(data, window=5):
if len(data) < window:
return data
return np.convolve(data, np.ones(window)/window, mode='valid')
# 繪製對比圖
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.figure(figsize=(12,6))
for bs in batch_sizes:
smoothed = smooth(aligned_losses[bs], window=5)
plt.plot(smoothed, label=f'Batch Size = {bs}', alpha=0.9)
plt.title("不同 Batch Size 下的 Mini-batch 損失波動")
plt.xlabel("迭代步")
plt.ylabel("訓練損失")
plt.legend()
plt.grid(True)
plt.show()
#輸出
print("\n=== 平均每 batch 耗時與波動強度 ===")
for bs in batch_sizes:
std = np.std(results[bs])
print(f"Batch Size={bs:<3} | 平均每 batch 耗時: {times[bs]:.4f} 秒 | 損失波動標準差: {std:.4f}")
3.2 對比不同優化算法
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn import init
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
import numpy as np
import time
transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
dataset = datasets.ImageFolder(root='./cat_dog', transform=transform)
train_size = int(0.8 * len(dataset))
val_size = int(0.1 * len(dataset))
test_size = len(dataset) - train_size - val_size
train_dataset, _, _ = random_split(dataset, [train_size, val_size, test_size])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.hidden1 = nn.Linear(128 * 128 * 3, 1024)
self.hidden2 = nn.Linear(1024, 512)
self.hidden3 = nn.Linear(512, 128)
self.hidden4 = nn.Linear(128, 32)
self.hidden5 = nn.Linear(32, 8)
self.hidden6 = nn.Linear(8, 3)
self.relu = nn.ReLU()
self.output = nn.Linear(3, 1)
self.sigmoid = nn.Sigmoid()
init.xavier_uniform_(self.output.weight)
def forward(self, x):
x = self.flatten(x)
x = self.relu(self.hidden1(x))
x = self.relu(self.hidden2(x))
x = self.relu(self.hidden3(x))
x = self.relu(self.hidden4(x))
x = self.relu(self.hidden5(x))
x = self.relu(self.hidden6(x))
x = self.sigmoid(self.output(x))
return x
def train_and_record(optimizer_type, batch_size=16, epochs=20, lr=0.001):
loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
model = NeuralNetwork().to(device)
criterion = nn.BCELoss()
if optimizer_type == 'SGD':
optimizer = optim.SGD(model.parameters(), lr=lr, weight_decay=0.01)
elif optimizer_type == 'Momentum':
optimizer = optim.SGD(model.parameters(), lr=lr, momentum=0.9, weight_decay=0.01)
elif optimizer_type == 'RMSprop':
optimizer = optim.RMSprop(model.parameters(), lr=lr, alpha=0.99, weight_decay=0.01)
elif optimizer_type == 'Adam':
optimizer = optim.Adam(model.parameters(), lr=lr, betas=(0.9,0.999), weight_decay=0.01)
batch_losses = []
epoch_losses = [] # 每 epoch 平均損失
start_time = time.time()
for epoch in range(epochs):
model.train()
epoch_loss = 0.0
for images, labels in loader:
images, labels = images.to(device), labels.to(device).float().unsqueeze(1)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
batch_losses.append(loss.item())
epoch_loss += loss.item()
epoch_losses.append(epoch_loss / len(loader)) # 平均每個 epoch 的損失
avg_time = (time.time() - start_time) / (epochs * len(loader))
return batch_losses, epoch_losses, avg_time
optimizers = ['SGD', 'Momentum', 'RMSprop', 'Adam']
results = {}
epoch_results = {}
times = {}
print("開始訓練並記錄不同優化器的損失...")
for opt_name in optimizers:
print(f"\n 訓練優化器={opt_name} ...")
losses, epoch_losses, avg_t = train_and_record(opt_name)
results[opt_name] = losses
epoch_results[opt_name] = epoch_losses
times[opt_name] = avg_t
print(f"{opt_name} 平均每 batch 時間: {avg_t:.4f} 秒")
max_points = 300
aligned_losses = {}
for opt_name, losses in results.items():
if len(losses) > max_points:
idx = np.linspace(0, len(losses)-1, max_points).astype(int)
aligned_losses[opt_name] = [losses[i] for i in idx]
else:
aligned_losses[opt_name] = losses
def smooth(data, window=5):
if len(data) < window:
return data
return np.convolve(data, np.ones(window)/window, mode='valid')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.figure(figsize=(12,6))
final_losses = {}
for opt_name in optimizers:
smoothed = smooth(aligned_losses[opt_name], window=5)
plt.plot(smoothed, label=opt_name, alpha=0.9)
final_losses[opt_name] = smoothed[-1] if len(smoothed) > 0 else np.nan
plt.title("不同優化算法下的 Mini-batch 損失下降對比(Batch Size=16)")
plt.xlabel("迭代步")
plt.ylabel("訓練損失")
plt.legend()
plt.grid(True)
plt.show()
plt.figure(figsize=(10,5))
for opt_name in optimizers:
plt.plot(epoch_results[opt_name], marker='o', label=opt_name)
plt.title("不同優化器下每 Epoch 平均損失對比")
plt.xlabel("Epoch")
plt.ylabel("平均損失")
plt.legend()
plt.grid(True)
plt.show()
#輸出
print("\n=== 平均每 batch 耗時與波動強度 ===")
for opt_name in optimizers:
std = np.std(results[opt_name])
print(f"{opt_name:<9} | 平均每 batch 耗時: {times[opt_name]:.4f} 秒 | 損失波動標準差: {std:.4f}")