此分類用於記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:
- 原課程視頻鏈接:[雙語字幕]吳恩達深度學習deeplearning.ai
- github課程資料,含課件與筆記:吳恩達深度學習教學資料
- 課程配套練習(中英)與答案:吳恩達深度學習課後習題與答案
本篇為第二課第三週的課程習題和代碼實踐部分筆記。
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 的梯度公式非常簡單:
代入我們的例子:
- 對“貓”神經元:\(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路徑中發現這樣的一個文件夾:
這是一個十分類數據集,包含七萬張手寫數字圖像。可以以此對手寫數字的圖像進行分類,如果訓練的模型較為成功,那麼我們就可以得到一個可以識別手寫數字的分類器。
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 第一次結果分析: 多分類
現在,我們根據上面的設置,來看看訓練結果:
如果你看過之前的幾次代碼實踐,可能會有一些疑惑,為什麼幾乎相同的配置下,貓狗二分類的準確率最高才剛剛到70%,現在都擴展到十分類並簡化了網絡結構的情況下,準確率卻在90%以上?
很明顯,二者最大的區別就是數據集不同。
我們來解釋一下為什麼手寫數字圖像識別的訓練效果這麼好:
- 貓狗數據集:圖像複雜、背景多變、光照、姿勢都可能不同,樣本間差異大,網絡需要學習的特徵複雜,因此訓練難度高,準確率提升較慢。
- 手寫數字 MNIST 數據集:圖像統一大小、灰度處理,數字相對居中,背景乾淨,樣本間差異小,網絡很容易學習到區分特徵,因此即使網絡結構相對簡單,也能快速達到高準確率。
簡單來説,就是手寫數字的數據好,圖像簡單,而數據的可分性和特徵明確程度直接決定了訓練效果。
因此,MNIST也常常作為圖神經網絡的入門教程,即使我們使用的是全連接網絡,也能達到較高的準確率,甚至你使用sigmoid和二分類交叉熵也能達到較好的擬合效果。
究其根本,數據好,就像品質極佳的原材料,就是水煮一下,也十分美味。
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前後的對比:
經過多次測試,可以較明顯的發現,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()