博客 / 詳情

返回

吳恩達深度學習課程二: 改善深層神經網絡 第三週:超參數調整,批量標準化和編程框架 課後習題和代碼實踐

此分類用於記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:

  1. 原課程視頻鏈接:[雙語字幕]吳恩達深度學習deeplearning.ai
  2. github課程資料,含課件與筆記:吳恩達深度學習教學資料
  3. 課程配套練習(中英)與答案:吳恩達深度學習課後習題與答案

本篇為第二課第三週的課程習題和代碼實踐部分筆記。


1. 理論習題:獨熱編碼

還是先上鍊接:
【中英】【吳恩達課後測驗】Course 2 - 改善深層神經網絡 - 第三週測驗
因為本週內容大多是一些補充,因此習題也大多隻是之前瞭解到的死知識,就不再多提了。
這部分補充一個之前出現的技術:獨熱編碼

1.1 獨熱編碼(One-Hot Encoding)

之前在多值預測與多分類這部分裏,我們提到過,在多分類的情況下, 使用獨熱編碼來表示各個分類,現在就來展開一下這個技術。
為了讓解釋更直觀,我們全程使用同一個實例:

例子:識別動物類別,有三類:貓(Cat)、狗(Dog)、兔子(Rabbit)。
我們用這個例子貫穿整個獨熱編碼的説明。

(1)用獨熱編碼表示分類的直接形式

在多分類中,一個類別就是一個“離散的標籤”,沒有數值大小,也不存在“比誰大一點”的概念
但神經網絡輸出的是一組數字,為了讓網絡能理解“哪個類別是正確答案”,我們就需要把“貓/狗/兔子”變成神經網絡能處理的格式。

獨熱編碼就是最直接的方式:每一個類別對應一個位置,正確的那個位置為 1,其餘為 0。
來看看具體怎麼做:現在對我們的動物識別例子做獨熱編碼處理,結果如下:

類別 獨熱編碼
貓 Cat [1, 0, 0]
狗 Dog [0, 1, 0]
兔 Rabbit [0, 0, 1]

其中:

  • 三個神經元對應三個分類
  • “1”表示正確分類,“0”表示不是
  • 標籤永遠只有一個位置是 1

這就是“獨熱”:只有一個地方熱
這是它對多分類的直接表現形式。

(2)為什麼二分類不使用獨熱編碼?

那你可能會問:
“既然多分類用獨熱,那二分類是不是也能寫成 [1,0][0,1]?”
答案:理論上可以,實踐中不會這麼做
原因很簡單:沒必要
簡單展開一下:
二分類的本質是:是否屬於某一類(比如“是不是貓”)

只需要一個神經元 + sigmoid,就能表達“是的概率”。
這意味着 一個神經元就能表達整個二分類的狀態

這種結構浪費計算,還會帶來梯度重複問題。
什麼叫“梯度重複”?,這是softmax在二分類應用中出現的問題。

假設某張圖真實標籤是:

貓 → [1, 0]

而模型預測是:

ŷ = [0.4, 0.6]

也就是模型認為“不是貓”的概率比“是貓”還高。
根據交叉熵,我們得到兩個神經元的損失項:

L1 = -1 * log(0.4)
L2 = -0 * log(0.6)

乍看只第一個有影響。
但真正計算梯度時,Softmax 會讓兩個神經元一起參與:

  • 第 1 個神經元(貓)要把概率從 0.4 推到更高
  • 第 2 個神經元(不是貓)要把概率從 0.6 推到更低

於是反向傳播時兩個神經元都會更新:

  • 第一個神經元:“我應該更強一點”
  • 第二個神經元:“我應該更弱一點”
    這就產生了兩個方向相反但意義重複的梯度

而這兩個神經元本質上是一件事:

P(不是貓) = 1 − P(是貓)

所以,這種結構就是讓網絡:

  • 學一次“貓應該更強”
  • 再學一次“不是貓應該更弱”

這其實是同一條語義的兩次更新

這就是二分類使用softmax帶來的梯度重複現象:
它讓模型參數增加,訓練更慢,softmax還讓兩個輸出互相牽連,一個升另一降,讓本來很簡單的二分類被人為增加了耦合難度。

(3) 多分類不使用獨熱編碼的影響

那多分類為什麼不能像二分類一樣直接寫成0,1,2呢?
就像這樣:

類別 非獨熱寫法
0
1
2

你可能已經發現了問題所在,我們在一開始就強調了:類別不存在“比誰大一點”的概念
使用上面這種分類方法帶倆的嚴重問題就是:神經網絡會錯誤地認為“兔 > 狗 > 貓”

再簡單展開一下:
在這種分類方式下,模型會把 “誤差”理解為數值距離

例如真實標籤是“兔 = 2”,模型預測成“貓 = 0”。
模型認為誤差 = |2 − 0| = 2
那預測成“狗 1”誤差就會變小。
於是, 模型會錯誤地認為預測成“狗”比預測成“貓”更接近正確答案,帶來梯度的混亂
但實際上,我們知道:“貓”和“狗”與“兔”之間沒有“更近”的關係,它們應該是三種平行的、不可比較的類別
所以這種寫法會導致訓練邏輯錯誤,學習方向混亂,效果極差。

(4)獨熱編碼對多分類的適配性

現在再來看看獨熱編碼的優勢。
繼續使用我們動物識別例子:
真實標籤“兔子” → [0, 0, 1]
假設模型輸出的是 Softmax 後的概率:

預測為:貓   0.1
預測為:狗   0.2
預測為:兔   0.7

Softmax 輸出為:

ŷ = [0.1, 0.2, 0.7]

真實標籤為:

y  = [0, 0, 1]

交叉熵損失就很自然:

Loss = - log(預測為兔的概率) = -log(0.7)

只有正確類別那一項會參與計算,其餘項為 0,不影響損失。

但重點來了:雖然損失項只有一項,但梯度來自所有類別
上面的損失表達式容易讓人誤解:“只有一項有用,那是不是梯度也只來自那一項?”
其實不是。

我們繼續看:
Softmax + CrossEntropy 的梯度公式非常簡單:

\[\frac{\partial L}{\partial z_i} = \hat{y}_i - y_i \]

代入我們的例子:

  • 對“貓”神經元:\(0.1 - 0 = 0.1\)
  • 對“狗”神經元:\(0.2 - 0 = 0.2\)
  • 對“兔”神經元:\(0.7 - 1 = -0.3\)
    可以看到:
  • “兔” 的梯度是負的 → 相關參數會變大(讓概率更接近 1)
  • “貓”和“狗”的梯度是正的 → 相關參數會變小(讓概率更接近 0)

這恰好符合我們對多分類的直觀理解: 正確類變得更確定,其他類一起被壓下去。

這就是多分類中,獨熱編碼,softmax,交叉熵形成的更新鏈條,我們在下面的實踐部分就能感受到它的效果。

2. 代碼實踐

在課程要求裏,這周的實踐作業是Tensorflow的入門,主要以瞭解Tensorflow的基本原理和語法為主,還是把這位博主的鏈接放在前面,介紹了使用Tensorflow構建神經網絡的過程。
【中文】【吳恩達課後編程作業】Course 2 - 改善深層神經網絡 - 第三週作業

雖然依然使用Pytorch來進行演示,但隨着引入Tensorflow框架,之後課程內容對此的介紹和使用也會增加。因此,之後我都會在最後附上一個Tensorflow版本的代碼。

2.1 多分類數據集

為了演示本週的內容,我們暫時放下之前的貓狗二分類數據集。
這次,我們使用一個新的數據集:手寫數字圖像識別
你可能之前已經知道這個數據集了,它並不需要我們和之前一樣在網上尋找數據集下載。
pytorch內置了這個經典數據集的下載鏈接,我們可以直接通過API下載它到項目目錄:

from torchvision import datasets, transforms  
from torch.utils.data import DataLoader
# 載入訓練數據集  
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)  
# 載入測試數據集  
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

運行後,你就會在你設置的root路徑中發現這樣的一個文件夾:
image.png
這是一個十分類數據集,包含七萬張手寫數字圖像。可以以此對手寫數字的圖像進行分類,如果訓練的模型較為成功,那麼我們就可以得到一個可以識別手寫數字的分類器。

2.2 網絡結構

根據我們在本週所瞭解的內容,再結合數據集的情況,我們設計新的網絡結構如下:

class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28*28, 512)  
        # 灰度圖只有一個通道來表示亮暗程度,不用像彩色圖像一樣乘3。
        self.hidden2 = nn.Linear(512, 256)  
        self.hidden3 = nn.Linear(256, 128)  
        self.hidden4 = nn.Linear(128, 32)  
        self.relu = nn.ReLU()  
        # 輸出層(使用Softmax進行多分類)  
        self.output = nn.Linear(32, 10)  # 輸出10個類別(0-9)  
        self.softmax = nn.Softmax(dim=1) 
        # dim=1:對每一行(即每個樣本的所有類別分數)進行計算,將每個類別的分數轉化為概率。
        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.output(x)  
        x = self.softmax(x)  # 使用Softmax輸出類別概率  
        return x

2.3 損失函數和其他設置

# 迭代設置
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)  
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)
# 圖像較簡單,因此增加批次大小到64

# 損失函數和優化器  
criterion = nn.CrossEntropyLoss()  # 多分類使用交叉熵損失  
optimizer = optim.Adam(model.parameters(), lr=0.001) # 優化器默認選擇Adam
num_epochs = 10 # 訓練十輪

這裏要單獨説明的是,我們上面瞭解到的對多分類的獨熱編碼就被封裝在CrossEntropyLoss損失函數的設置裏,它內部會自動把標籤整數轉為獨熱的形式進行計算。

2.4 第一次結果分析: 多分類

現在,我們根據上面的設置,來看看訓練結果:
image.png
如果你看過之前的幾次代碼實踐,可能會有一些疑惑,為什麼幾乎相同的配置下,貓狗二分類的準確率最高才剛剛到70%,現在都擴展到十分類並簡化了網絡結構的情況下,準確率卻在90%以上?
很明顯,二者最大的區別就是數據集不同。
我們來解釋一下為什麼手寫數字圖像識別的訓練效果這麼好:

  • 貓狗數據集:圖像複雜、背景多變、光照、姿勢都可能不同,樣本間差異大,網絡需要學習的特徵複雜,因此訓練難度高,準確率提升較慢。
  • 手寫數字 MNIST 數據集:圖像統一大小、灰度處理,數字相對居中,背景乾淨,樣本間差異小,網絡很容易學習到區分特徵,因此即使網絡結構相對簡單,也能快速達到高準確率。

簡單來説,就是手寫數字的數據好,圖像簡單,而數據的可分性和特徵明確程度直接決定了訓練效果。
因此,MNIST也常常作為圖神經網絡的入門教程,即使我們使用的是全連接網絡,也能達到較高的準確率,甚至你使用sigmoid和二分類交叉熵也能達到較好的擬合效果。

究其根本,數據好,就像品質極佳的原材料,就是水煮一下,也十分美味。
20251124114508842

2.5 加入批量標準化

我們本週瞭解了batch歸一化,知道了它能起到加速訓練,同時有輕微正則化的作用。
現在,我們就再把BN加入數字圖像識別模型。
在Pytorch中,BN也被封裝在網絡結構模塊裏,完善後如下:

class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28 * 28, 512)  
        self.bn1 = nn.BatchNorm1d(512)  # 第一層的BN
        self.hidden2 = nn.Linear(512, 256)  
        self.bn2 = nn.BatchNorm1d(256)  # 第二層的BN
        self.hidden3 = nn.Linear(256, 128)  
        self.bn3 = nn.BatchNorm1d(128)  # 第三層的BN
        self.hidden4 = nn.Linear(128, 32)  
        self.bn4 = nn.BatchNorm1d(32)  # 第四層的BN
        self.relu = nn.ReLU()  
        self.output = nn.Linear(32, 10)  
        self.softmax = nn.Softmax(dim=1)  
        init.xavier_uniform_(self.output.weight)  
    # 把BN加入傳播過程
    def forward(self, x):  
        x = self.flatten(x)  
        x = self.hidden1(x)  
        x = self.bn1(x)  # 這裏 
        x = self.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x)  # 這裏
        x = self.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x)  # 這裏
        x = self.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x)  # 這裏
        x = self.relu(x)  
        x = self.output(x)  
        x = self.softmax(x)  
        return x

於此同時,我們記得BN在訓練和測試中對參數的使用有差別,測試中會使用訓練中的全局均值和全局方差。
而這個邏輯是通過訓練模式和評估模式的轉換完成的:

model.train()  # 訓練中維護全局 BN 參數
····訓練代碼
model.eval()   # 測試中使用固定全局 BN 參數

現在我們再來看看結果。

2.6 第二次結果分析:加入BN

來看看加入BN前後的對比:
20251124124510355
經過多次測試,可以較明顯的發現,BN起到了加速訓練的作用,在相同的其他配置下,使用BN的模型準確率也高於不使用BN。

3.附錄

3.1 PyTorch版:數字圖像識別模型代碼

import torch  
import torch.nn as nn  
import torch.optim as optim  
from torchvision import datasets, transforms  
from torch.utils.data import DataLoader  
from torch.nn import init  
import matplotlib.pyplot as plt  
  
transform = transforms.Compose([  
    transforms.ToTensor(),  
    transforms.Normalize((0.5,), (0.5,))  
])  
# 載入訓練數據集  
train_dataset = datasets.MNIST(  
    root='./data',  
    train=True,  
    download=True,  
    transform=transform  
)  
test_dataset = datasets.MNIST(  
    root='./data',  
    train=False,  
    download=True,  
    transform=transform  
)  
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)  
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)  
  
class NeuralNetwork(nn.Module):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = nn.Flatten()  
        self.hidden1 = nn.Linear(28 * 28, 512)  
        self.bn1 = nn.BatchNorm1d(512)  
        self.hidden2 = nn.Linear(512, 256)  
        self.bn2 = nn.BatchNorm1d(256)  
        self.hidden3 = nn.Linear(256, 128)  
        self.bn3 = nn.BatchNorm1d(128)  
        self.hidden4 = nn.Linear(128, 32)  
        self.bn4 = nn.BatchNorm1d(32)  
        self.relu = nn.ReLU()  
        self.output = nn.Linear(32, 10)  
        self.softmax = nn.Softmax(dim=1)  
        init.xavier_uniform_(self.output.weight)  
  
    def forward(self, x):  
        x = self.flatten(x)  
        x = self.hidden1(x)  
        x = self.bn1(x)  
        x = self.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x)  
        x = self.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x)  
        x = self.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x)  
        x = self.relu(x)  
        x = self.output(x)  
        x = self.softmax(x)  
        return x  
  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
model = NeuralNetwork().to(device)  
  
criterion = nn.CrossEntropyLoss()  
optimizer = optim.Adam(model.parameters(), lr=0.001)  
  
  
num_epochs = 10  
train_losses, train_accuracies, test_accuracies = [], [], []  
# 訓練  
for epoch in range(num_epochs):  
    model.train()  
    running_loss = 0.0  
    correct_train = 0  
    total_train = 0  
    for images, labels in train_loader:  
        images, labels = images.to(device), labels.to(device)  
        optimizer.zero_grad()  
        outputs = model(images)  
        loss = criterion(outputs, labels)  
        loss.backward()  
        optimizer.step()  
        running_loss += loss.item() * images.size(0)  
        _, predicted = torch.max(outputs, 1)  
        total_train += labels.size(0)  
        correct_train += (predicted == labels).sum().item()  
  
    epoch_loss = running_loss / len(train_loader.dataset)  
    train_accuracy = correct_train / total_train  
    train_losses.append(epoch_loss)  
    train_accuracies.append(train_accuracy)  
  
    # 測試  
    model.eval()  
    correct_test = 0  
    total_test = 0  
    with torch.no_grad():  
        for images, labels in test_loader:  
            images, labels = images.to(device), labels.to(device)  
            outputs = model(images)  
            _, predicted = torch.max(outputs, 1)  
            total_test += labels.size(0)  
            correct_test += (predicted == labels).sum().item()  
    test_accuracy = correct_test / total_test  
    test_accuracies.append(test_accuracy)  
  
    print(f"Epoch {epoch + 1}/{num_epochs} | Loss: {epoch_loss:.4f} | "          f"Train Acc: {train_accuracy:.4f} | Test Acc: {test_accuracy:.4f}")  
  
# 可視化  
plt.figure(figsize=(10, 5))  
plt.plot(train_losses, label='Train Loss', marker='o')  
plt.plot(train_accuracies, label='Train Accuracy', marker='x')  
plt.plot(test_accuracies, label='Test Accuracy', marker='s')  
plt.title('Training Loss & Accuracy')  
plt.xlabel('Epoch')  
plt.ylabel('Value')  
plt.ylim(0, max(max(train_losses), 1.0) + 0.1)  
plt.grid(True)  
plt.legend()  
plt.show()

3.2 Tensorflow版:數字圖像識別模型代碼

import tensorflow as tf  
from tensorflow.keras import layers, optimizers, losses  
import matplotlib.pyplot as plt  
  
# 載入 MNIST 數據  
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()  
# 定義模型類  
class NeuralNetwork(tf.keras.Model):  
    def __init__(self):  
        super(NeuralNetwork, self).__init__()  
        self.flatten = layers.Flatten()  
        self.rescale = layers.Rescaling(1. / 127.5, offset=-1)  # [-1,1] 歸一化  
        self.hidden1 = layers.Dense(512)  
        self.bn1 = layers.BatchNormalization()  
        self.hidden2 = layers.Dense(256)  
        self.bn2 = layers.BatchNormalization()  
        self.hidden3 = layers.Dense(128)  
        self.bn3 = layers.BatchNormalization()  
        self.hidden4 = layers.Dense(32)  
        self.bn4 = layers.BatchNormalization()  
        self.output_layer = layers.Dense(10, activation='softmax')  
  
    def call(self, x, training=False):  
        x = self.flatten(x)  
        x = self.rescale(x)  
        x = self.hidden1(x)  
        x = self.bn1(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden2(x)  
        x = self.bn2(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden3(x)  
        x = self.bn3(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.hidden4(x)  
        x = self.bn4(x, training=training)  
        x = tf.nn.relu(x)  
        x = self.output_layer(x)  
        return x  
  
  
# 實例化模型  
model = NeuralNetwork()  
  
# 編譯模型:加設置  
model.compile(optimizer=optimizers.Adam(learning_rate=0.001),  
              loss=losses.SparseCategoricalCrossentropy(),  
              metrics=['accuracy'])  
  
# 訓練模型  
num_epochs = 10  
batch_size = 64  
history = model.fit(x_train, y_train,  
                    validation_data=(x_test, y_test),  
                    epochs=num_epochs,  
                    batch_size=batch_size)  
  
# 可視化訓練曲線  
plt.figure(figsize=(10, 5))  
plt.plot(history.history['loss'], label='Train Loss', marker='o')  
plt.plot(history.history['accuracy'], label='Train Accuracy', marker='x')  
plt.plot(history.history['val_accuracy'], label='Test Accuracy', marker='s')  
plt.title('Training Loss & Accuracy')  
plt.xlabel('Epoch')  
plt.ylabel('Value')  
plt.ylim(0, max(max(history.history['loss']), 1.0) + 0.1)  
plt.grid(True)  
plt.legend()  
plt.show()
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.