吳恩達深度學習課程一:神經網絡和深度學習 第三周:淺層神經網絡 課后作業和代碼實踐
此分類用于記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:
- 原課程視頻鏈接:
- github課程資料,含課件與筆記:
- 課程配套練習(中英)與答案:
本篇為第一課第三周的課程練習部分的講解。
1.理論習題
同樣,這是本周理論部分的習題和相應解析,博主已經做好了翻譯和解析工作,因此便不再重復。
本周理論習題較為簡單,幾乎所有內容都在之前的理論部分提及過,涉及代碼的題也只是簡單的numpy語法問題,因此就不再就某道題展開了,我們將重點放在下面的代碼部分。
2.代碼實操:帶隱藏層的分類模型
同樣再次粘貼整理了課程習題的博主答案,博主依舊在不借助很多現在流行框架的情況下手動構建模型的各個部分,并手動實現傳播,計算過程。
如果希望更扎實地了解原理,更推薦跟隨這位博主的內容一步步手動構建模型,這樣對之后框架的使用也會更得心應手。
當然,同樣會在這里給出我的另一個更偏向使用現有框架和內置函數的版本以供參考。
在第二周的代碼實踐部分我們用邏輯回歸來訓練一個貓狗圖像二分類模型,但是受制于數據集,模型結構等各種因素,最終的模型測試結果并不優秀,只有58%左右的準確率。
而本周我們了解了淺層神經網絡的各部分原理,知道了其如何提高擬合效果,現在便延續上一周的內容,再次在這個數據集上應用本周更新的內容,來看一看效果。
2.1 邏輯回歸模型代碼
先回看一下之前的模型代碼:
class LogisticRegressionModel(nn.Module):
# 類繼承自nn.Module,是 PyTorch 所有模型的基類
#初始化方法
def __init__(self):
super().__init__() #父類初始化,用于注冊子模塊等,涉及源碼,這里當成固定即可。
self.flatten = nn.Flatten() #把張量后三維展平為一維(通道C*高H*寬W)
self.linear = nn.Linear(128 * 128 * 3, 1) # 輸入是128x128x3,輸出1個加權和
# nn.Linear接受的是二維輸入[batch_size, features],這里是[32,128 * 128 * 3]
# 但Linear層不需要在參數里寫 batch 維度,它內部會自動處理批量輸入,只關心每個樣本的特征數和每個樣本輸出的維度,這也是廣播機制的應用。
self.sigmoid = nn.Sigmoid() #激活函數
#向前傳播方法
def forward(self, x):
# 現在,x的維度是[32,3,128,128]
x = self.flatten(x) #1.展平
# 現在,x的維度是[32,128 * 128 * 3]
x = self.linear(x) #2.過線性組合得到加權和
# 現在,x的維度是[32,1]
x = self.sigmoid(x) #3.過激活函數得到輸出
# 現在,x的維度是[32,1]
return x
雖然注釋較多,但其原理并不復雜,我們看向前傳播函數,實際上一次正向傳播只有三步顯式計算。
2.2 淺層神經網絡模型代碼
class ShallowNeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten() #
# 隱藏層:輸入128*128*3維特征,輸出4個神經元
self.hidden = nn.Linear(128 * 128 * 3, 4)
# 隱藏層激活函數:ReLU,作用是加入非線性特征
self.ReLU = nn.ReLU()
# 輸出層:將隱藏層的 4 個輸出映射為 1 個加權和
self.output = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid()
# 前向傳播方法
def forward(self, x):
# 輸入 x 的維度為 [32, 3, 128, 128]
x = self.flatten(x)
# 展平后 x 的維度為 [32, 128 * 128 * 3]
x = self.hidden(x)
# 通過隱藏層線性變換后,x 的維度為 [32, 4]
x = self.ReLU(x)
# 經過 ReLU 激活后形狀不變,仍為 [32, 4]
x = self.output(x)
# 通過輸出層得到加權和,形狀變為 [32, 1]
x = self.sigmoid(x)
# 經過 sigmoid 激活后得到 0~1 的概率值,形狀仍為 [32, 1]
return x
這就是設置了四個隱藏層神經元的淺層神經網絡的代碼,可以發現,在Pytorch的內置方法幫助下,我們只不過相對邏輯回歸改動了幾行代碼,便實現了從邏輯回歸到淺層神經網絡進行二分類的效果。
2.3 驗證模型優化
現在,我們依舊使用上周的代碼,只將模型替換為淺層神經網絡而不改變其他任何部分(完整代碼附在文末)。
先來看一張測試結果:

如果只看這兩張圖的對比,可能會想,只是簡單的擴大一下網絡的規模就實現了準確率的提升,確實差別展示的非常明顯。
但可惜,事實并非如此,實際上,圖中右側的結果只是多次運行結果中最好的一次,幾乎不可控,就像抽卡游戲里非常小概率的金卡一樣。而實際上,最終測試的準確率依舊在50%到60%之間徘徊。
那問題又來了,那增加隱藏層就一點作用也沒有嗎?
答案當然是否定的,我們再來看一組對比:

我們觀察兩幅圖像,會很明顯的發現區分。
- 在邏輯回歸的圖像中,準確率的曲線幾乎看不出幅度,而在淺層神經網絡中,它甚至已經和損失曲線相相交了。
回看剛剛每輪的損失,我們會發現,造成這種差別的原因是:準確率只在0到1間波動,而邏輯回歸的平均損失卻在1-10這個量級內,相比之下,淺層神經網絡的平均損失已經降到了0到1之間。 - 邏輯回歸的損失在數值高的情況下,相比淺層神經網絡的波動也較大,而淺層神經網絡的下降趨勢幾乎成了一條直線。
而這是因為邏輯回歸因為模型太簡單、只能學習線性關系,訓練時更容易在高損失區震蕩;而淺層神經網絡具備初步非線性表達能力,能更穩定地逼近最優解,損失下降更平滑、更接近直線。
從這方面來看,增加隱藏層又有其提升。
可是還有一個問題,損失函數是用來反向傳播更新參數從而提高擬合效果,提高準確率的,那現在損失變小準確率不提升是為什么?
在這次代碼實踐中,我們重點討論一下這個問題。
2.4 為什么損失減少不增加準確率?
雖然損失函數的值在不斷下降,但準確率卻沒有顯著提高,這并不矛盾,我們來細究一下:
首先,損失函數本質上衡量的是模型輸出概率與真實標簽的差距,而準確率衡量的是最終分類結果是否正確,兩者關注的維度不同。
當模型預測的概率逐漸接近真實標簽(例如從 0.49 提高到 0.51 或從 0.4 提高到 0.6),損失會明顯下降,但是只要這些概率在 0.5 的分類閾值附近徘徊,模型的預測仍可能被判定為錯誤,于是準確率就不會顯著改變。
更進一步來說,在模型訓練初期,損失的下降主要來自于“模型更有信心地接近正確答案”,但這種接近往往還不足以跨過分類邊界。
例如,對于真實標簽為 1 的樣本,輸出可能從 0.3 提升到 0.4,這的確讓損失下降了很多,但它仍被判斷為 0 類,這對準確率沒有任何貢獻。同樣地,若對于真實為 0 的樣本,輸出從 0.8 降為 0.6,也會使損失變小,但預測依舊是錯誤的類別,如圖所示:

還有一種情況也會導致損失在下降,但準確率“卡住不動”,那就是數據本身太復雜。 我們可以把它想象成兩類數據混在一起,不是涇渭分明地分成左右兩堆,而是交錯、環繞,甚至夾雜著噪聲。淺層神經網絡雖然比邏輯回歸更聰明一點,但它的“想象力”依然有限,只能學會把整體誤差變小,卻畫不出一條真正把兩類數據明顯分開的界線,就像這張圖:

因此,在本次實操中我們會發現:損失下降說明模型確實在學習,但如果模型能力不足、特征沒有被有效分離,或者分類閾值附近的樣本占比過高,那么準確率依舊可能停留在50%到60%之間。
而如何進一步提升準確率,便需要我們繼續學習,繼續優化。
最后附上完整代碼,依舊要強調的是,在規范流程里,我們應該根據每次驗證的準確率調整超參數,最后再進行測試,只是這部分內容還在后面,我們經過系統學習后再正式引入這部分。
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score
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, val_dataset, test_dataset = random_split(dataset, [train_size, val_size, test_size])
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)
class ShallowNeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
# 隱藏層
self.hidden = nn.Linear(128 * 128 * 3, 4) # 1 個隱藏層,4 個神經元
self.ReLU = nn.ReLU()
# 輸出層
self.output = nn.Linear(4, 1)
self.sigmoid = nn.Sigmoid()
def forward(self, x):
x = self.flatten(x)
x = self.hidden(x)
x = self.ReLU(x)
x = self.output(x)
x = self.sigmoid(x)
return x
model = ShallowNeuralNetwork()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
criterion = nn.BCELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
epochs =10
train_losses = []
val_accuracies = []
for epoch in range(epochs):
model.train()
epoch_train_loss = 0
for images, labels in train_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()
epoch_train_loss += loss.item()
avg_train_loss = epoch_train_loss / len(train_loader)
train_losses.append(avg_train_loss)
model.
val_true, val_pred = [], []
with torch.no_grad():
for images, labels in val_loader:
images = images.to(device)
outputs = model(images)
preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()
val_pred.extend(preds)
val_true.extend(labels.numpy())
val_acc = accuracy_score(val_true, val_pred)
val_accuracies.append(val_acc)
print(f"輪次: [{epoch + 1}/{epochs}], 訓練損失: {avg_train_loss:.4f}, 驗證準確率: {val_acc:.4f}")
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.plot(train_losses, label='訓練損失')
plt.plot(val_accuracies, label='驗證準確率')
plt.title("訓練損失與驗證準確率隨輪次變化圖")
plt.xlabel("訓練輪次(Epoch)")
plt.ylabel("數值")
plt.legend()
plt.grid(True)
plt.show()
model.
y_true, y_pred = [], []
with torch.no_grad():
for images, labels in test_loader:
images = images.to(device)
outputs = model(images)
preds = (outputs.cpu().numpy() > 0.5).astype(int).flatten()
y_pred.extend(preds)
y_true.extend(labels.numpy())
acc = accuracy_score(y_true, y_pred)
print(f"測試準確率: {acc:.4f}")
