此分類用於記錄吳恩達深度學習課程的學習筆記。
課程相關信息鏈接如下:
- 原課程視頻鏈接:[雙語字幕]吳恩達深度學習deeplearning.ai
- github課程資料,含課件與筆記:吳恩達深度學習教學資料
- 課程配套練習(中英)與答案:吳恩達深度學習課後習題與答案
本篇為第四課第一週的課後習題和代碼實踐部分。
1. 理論習題
【中英】【吳恩達課後測驗】Course 4 -卷積神經網絡 - 第一週測驗
本週的題多是一些尺寸和參數量的計算,只要對公式和層級結構足夠熟練,就沒什麼大問題。
來看看這道可能容易混淆的題:
把下面這個過濾器應用到灰度圖像會怎麼樣?
答案:檢測豎直邊緣。
這道題乍一看可能會有些迷惑,但是觀察就會發現,左右數字對稱,符號相反。
也就是説,如果應用這個過濾器,當對應區域左右像素接近時,結果就幾乎為0。但當左右像素出現較大差別時,結果的絕對值就會較大。
這就是豎直邊緣的邏輯,如果把整個矩陣旋轉 90 度,檢測的就是水平邊緣,只是二者的效果可能都沒有我們常用的邊緣檢測過濾器好。
2. 代碼實踐
吳恩達卷積神經網絡實戰
同樣,這位博主還是手工構建了卷積網絡中的各個組件,有興趣可以鏈接前往。
我們還是用 PyTorch 來進行演示,終於正式引入了卷積網絡,還是用貓狗二分類來看看卷積網絡在圖學習中的效果。
首先來看看 PyTorch 中如何定義卷積層:
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=0, stride=1)
# Conv2d 是指二維卷積,雖然圖片可以有多個通道,但它實際上還是二維的”紙片人“。
# Conv3d 便適用於視頻和 3D 圖片這樣的三維數據。
# 3,16 是指輸入和輸入的通道數,必須顯式指定。
# kernel_size=3 是卷積核尺寸,必須顯式指定。
# padding=0 ,0 就是padding 的默認值。
# stride 就是步長,1 就是步長的默認值。
再看看池化層:
self.max_pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
# MaxPool2d 即為最大池化。
# 在池化層中,stride 默認和 kernel_size 相同。
# 池化層會自適應輸入通道數,因此不用顯示指定。
self.avg_pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
# 同理,AvgPool2d 就是平均池化。
現在,我們就來看看卷積網絡的使用效果。
2.1 卷積網絡 1.0
我們現在設計卷積網絡如下:
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 卷積層
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# 全連接層
self.fc1 = nn.Linear(64 * 16 * 16, 128)
self.fc2 = nn.Linear(128, 1)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x) # 另一種調用激活函數的方式
x = self.pool(x)
x = self.conv2(x)
x = F.relu(x)
x = self.pool(x)
x = self.conv3(x)
x = F.relu(x)
x = self.pool(x)
x = torch.flatten(x, 1) # 進入全連接層前要先展平
x = self.fc1(x)
x = self.fc2(x)
x = torch.sigmoid(x)
return x
來看看運行結果如何:
可以看到,僅僅經過 20 輪訓練,訓練準確率就幾乎達到 100% ,但是驗證準確率卻仍在70%左右徘徊。
這是典型的過擬合現象:模型的學習能力很強,但是泛化能力不好。
經過前面的內容,我們已經瞭解了很多可以緩解過擬合現象的方法。現在,我們就開始一步步調試,緩解過擬合現象,增強模型的泛化能力。
2.2 卷積網絡 2.0:加入 Dropout
我們在正則化部分了解了可以通過應用 dropout 來緩解過擬合,現在就來看看效果。
如果你有些忘了什麼是dropout,它的第一次出現在這裏:dropout正則化
應用 dropout 後,我們更新網絡結構如下:
class SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 卷積層
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# 全連接層
self.fc1 = nn.Linear(64 * 16 * 16, 128)
self.fc2 = nn.Linear(128, 1)
# dropout
self.dropout = nn.Dropout(p=0.3)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x) # dropout
x = self.conv2(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x) # dropout
x = self.conv3(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x) # dropout
x = torch.flatten(x, 1)
x = self.fc1(x)
x = self.dropout(x) # dropout
x = self.fc2(x)
x = torch.sigmoid(x)
return x
現在再來看看結果:
你會發現,dropout 確實有作用,增加訓練輪次為 30 輪,訓練集上的準確率上升的沒有那麼快了,確實和驗證準確率的差距更小了。
但問題是驗證準確率也沒上去啊! 原本的過擬合問題,現在變成了欠擬合問題:模型對數據的擬合能力不足,而且通過趨勢,會發現如果繼續訓練,仍存在過擬合風險。
那該怎麼辦呢?
我們知道,無論是過擬合還是欠擬合,我們希望提高模型性能,最直接的方法就是增加數據量。
我們先不急着上網找圖片,不如就先試試我們之前經常提到的數據增強,看看效果如何。
2.3 卷積網絡 3.0:進行數據增強
現在,我們要進行數據增強,那麼要修改的代碼內容就換到了預處理部分。
同樣,數據增強第一次出現在這裏:其他緩解過擬合的方法
現在,我們仍然保留上一步的 dropout 內容,修改預處理代碼如下:
transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(),# 圖片有 50% 可能水平翻轉
transforms.RandomRotation(10), # 在 角度 -10° 到 10° 之間隨機旋轉圖像。
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
再再來看看效果如何:
不是,好像變化不大啊!
彆着急,之前我們一直在關注準確率,但是你會發現,在相同的訓練輪次下,損失仍在平穩下降,但是下降的更慢了,並沒有達到最開始過擬合的損失水平,比剛加入 dropout 時的損失還要高。
這説明:我們還沒有達到模型的上限。
現在,繼續維持其他內容不變,只增加訓練輪次,我們再來運行看看。
現在,把訓練輪次增加到100輪,就可以發現:驗證準確率有了一定的提升,但好像還是有過擬合風險,如果要繼續修改,可以選擇提高 dropout 的比率,也可以嘗試其他正則化。
簡單瞭解卷積層的性能後,我們就不在繼續調試了。
實際上,通過繼續修改網絡結構或者繼續增強數據,還可以讓模型有更多的提升,但就不在這裏演示了。
下一週的內容就是對現有的一些經典網絡結構的介紹,到時候,我們再來看看出色的網絡結構是什麼效果。
3.附錄
3.1 卷積網絡 3.0 pytorch代碼
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
transform = transforms.Compose([
transforms.Resize((128, 128)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
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 SimpleCNN(nn.Module):
def __init__(self):
super().__init__()
# 卷積層
self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)
self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)
self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
self.pool = nn.MaxPool2d(2, 2)
# 全連接層
self.fc1 = nn.Linear(64 * 16 * 16, 128)
self.fc2 = nn.Linear(128, 1)
self.dropout = nn.Dropout(p=0.3)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x)
x = self.conv2(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x)
x = self.conv3(x)
x = F.relu(x)
x = self.pool(x)
x = self.dropout(x)
x = torch.flatten(x, 1)
x = self.fc1(x)
x = self.dropout(x)
x = self.fc2(x)
x = torch.sigmoid(x)
return x
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleCNN().to(device)
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
epochs = 100
train_losses = []
train_accuracies = []
val_accuracies = []
for epoch in range(epochs):
model.train()
epoch_loss = 0
correct_train = 0
total_train = 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_loss += loss.item()
preds = (outputs > 0.5).int()
correct_train += (preds == labels.int()).sum().item()
total_train += labels.size(0)
avg_loss = epoch_loss / len(train_loader)
train_acc = correct_train / total_train
train_losses.append(avg_loss)
train_accuracies.append(train_acc)
model.eval()
correct_val = 0
total_val = 0
with torch.no_grad():
for images, labels in val_loader:
images, labels = images.to(device), labels.to(device).float().unsqueeze(1)
outputs = model(images)
preds = (outputs > 0.5).int()
correct_val += (preds == labels.int()).sum().item()
total_val += labels.size(0)
val_acc = correct_val / total_val
val_accuracies.append(val_acc)
print(f"輪次: [{epoch+1}/{epochs}], 訓練損失: {avg_loss:.4f}, 訓練準確率: {train_acc:.4f}, 驗證準確率: {val_acc:.4f}")
# 可視化
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.plot(train_losses, label='訓練損失')
plt.plot(train_accuracies, label='訓練準確率')
plt.plot(val_accuracies, label='驗證準確率')
plt.title("訓練損失、訓練準確率、驗證準確率變化曲線")
plt.xlabel("訓練輪次(Epoch)")
plt.ylabel("數值")
plt.legend()
plt.grid(True)
plt.show()
# 最終測試,可忽略
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).float().unsqueeze(1)
outputs = model(images)
preds = (outputs > 0.5).int()
correct_test += (preds == labels.int()).sum().item()
total_test += labels.size(0)
test_acc = correct_test / total_test
print(f"測試準確率: {test_acc:.4f}")
3.2 卷積網絡 3.0 TF版代碼
import tensorflow as tf
from tensorflow.keras import layers, models
import matplotlib.pyplot as plt
img_size = (128, 128)
batch_size = 32
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
'./cat_dog',
validation_split=0.2,
subset="training",
seed=42,
image_size=img_size,
batch_size=batch_size
)
val_test_ds = tf.keras.preprocessing.image_dataset_from_directory(
'./cat_dog',
validation_split=0.2,
subset="validation",
seed=42,
image_size=img_size,
batch_size=batch_size
)
val_size = int(0.5 * len(val_test_ds))
val_ds = val_test_ds.take(val_size)
test_ds = val_test_ds.skip(val_size)
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.1)
])
normalization = layers.Rescaling(1/0.5, offset=-1)
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.map(lambda x, y: (normalization(data_augmentation(x)), y)).prefetch(AUTOTUNE)
val_ds = val_ds.map(lambda x, y: (normalization(x), y)).prefetch(AUTOTUNE)
test_ds = test_ds.map(lambda x, y: (normalization(x), y)).prefetch(AUTOTUNE)
train_ds = train_ds.cache().shuffle(1000).prefetch(tf.data.AUTOTUNE)
val_ds = val_ds.cache().prefetch(tf.data.AUTOTUNE)
model = models.Sequential([
layers.Conv2D(16, (3, 3), padding='same', activation='relu', input_shape=img_size + (3,)),
layers.MaxPooling2D(2, 2),
layers.Dropout(0.3),
layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D(2, 2),
layers.Dropout(0.3),
layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
layers.MaxPooling2D(2, 2),
layers.Dropout(0.3),
layers.Flatten(),
layers.Dense(128, activation='relu'),
layers.Dense(1, activation='sigmoid')
])
model.summary()
model.compile(
optimizer=tf.keras.optimizers.Adam(1e-3),
loss='binary_crossentropy',
metrics=['accuracy']
)
epochs = 100
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=epochs
)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.plot(history.history['loss'], label='訓練損失')
plt.plot(history.history['accuracy'], label='訓練準確率')
plt.plot(history.history['val_accuracy'], label='驗證準確率')
plt.title("訓練損失、訓練準確率、驗證準確率變化曲線")
plt.xlabel("訓練輪次(Epoch)")
plt.ylabel("數值")
plt.legend()
plt.grid(True)
plt.show()
test_loss, test_acc = model.evaluate(test_ds)
print(f"測試準確率: {test_acc:.4f}")