博客 / 詳情

返回

吳恩達深度學習課程三: 結構化機器學習項目 第二週:誤差分析與學習方法 課後習題和代碼實踐

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

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

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


1 . 理論習題

還是先上鍊接:【中英】【吳恩達課後測驗】Course 3 -結構化機器學習項目
這兩週的理論習題都是對一些實際項目中的選擇策略,還是不多提了,我們把重點放在下面對本週瞭解的遷移學習和多任務學習的演示上。

2. 代碼實踐

2.1 遷移學習

這次就撿起來我們之前的貓狗二分類模型,之前我們嘗試訓練這個模型,多輪訓練後驗證集的最高準確率也只在 70% 上下波動,來看看對這個模型應用遷移學習的效果。
在對二分類模型應用遷移學習前,先簡單介紹一下我們的遷移來源任務

2.1.1 預訓練模型 ResNet18(ImageNet 預訓練)

我們使用的預訓練模型叫ResNet18,ResNet全稱為Residual Neural Network,中文翻譯為殘差神經網絡,18是指網絡深度。
這是一個非常經典且具有開創意義的模型結構,在大量的領域都能廣泛應用。而它最初的使用,就是在圖像分類上。
這裏擺一張網絡結構圖,暫時先不介紹它的結構和原理,課程在下一部分才正式介紹圖像學習的基本:卷積神經網絡,現在,只需要知道我們借來了一個很厲害的模型就好了。
image.png
注:這張圖來自這裏

而對於ImageNet,我們之前也提到過:ImageNet 是一個包含 1400 萬張圖像、覆蓋 1000 個類別 的大型圖像分類數據集。每張圖像都帶有準確的標籤,相當於給模型提供了大量“優質原材料”,讓它先在通用視覺特徵上打下堅實基礎。
image.png
注:這張則來自這裏
雖然ImageNet本身也包括了貓和狗的圖像,有一些“透題”的感覺,但1000 類中“貓狗”類比例極小(不到 0.5%),超多的類別並不會讓它特別偏向二者的識別,而是通用的紋理識別。
因此,經過ImageNet預訓練的ResNet18模型仍很適合作為我們的遷移來源模型。
下面就看看如何改動我們之前的代碼來引入它。

2.1.2 PyTorch引入預訓練ResNet18

現在,我們就從代碼層面看看如何使用預訓練ResNet18。

(1)修改預處理以適應ResNet18輸入層

首先,引入預訓練ResNet18就代表數據從原本的輸入我們創建的模型改為輸入ResNet18。
所以,我們首先就應該更改數據預處理方法以匹配ResNet18輸入層
而在PyTorch裏,負責數據預處理的就是transforms模塊。
先看看我們原來的預處理:

transform = transforms.Compose([  
    transforms.Resize((128, 128)), 
    # 將圖像的大小調整為 128x128 像素,保證輸入圖像的一致性 
    transforms.ToTensor(),
    # 將圖像從 PIL 圖像或 NumPy 數組轉換為 PyTorch 張量,圖像的像素值也會被從 [0, 255] 範圍映射到 [0, 1 ]範圍,這是使用 Pytorch 固定的一步。
    transforms.Normalize((0.5,), (0.5,))  
    # 標準化,原本在 [0, 1] 範圍內的像素值會變換到 [-1, 1] 範圍內。
])
·····中間代碼
self.hidden1 = nn.Linear(128 * 128 * 3, 1024) # 模型輸入層,對應輸入維度和Resize大小對應,* 3是因為彩色圖片有三個通道。

而現在,使用ResNet18,自然就要匹配他的一些輸入設置,所以,我們修改成如下設置:

transform = transforms.Compose([  
    transforms.Resize((224, 224)), # ResNet 輸入 224x224 這一步一定要有              
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406],  
                         [0.229, 0.224, 0.225])   # ResNet 官方均值方差  
    # 這些數字是基於 ImageNet 數據集 上的統計計算得到的,是針對每個通道的均值和方差。
])

如果你忘了標準化的是用來做什麼的,我們第一次介紹它是在這裏:歸一化
現在,我們的數據經過預處理就能順利輸入ResNet18了,我們繼續下一步。

(2)調用預訓練ResNet18模型

同樣,PyTorch 內置了 ResNet18 的模型結構,我們這樣調用它:

model = models.resnet18(pretrained=True)
# pretrained 參數的T和F就代表是否使用預訓練的模型參數,這裏的 True 就代表使用。
# 而如果更改為 False ,就代表只使用 ResNet18 的網絡結構。

注意,這時我們原本設計的網絡結構就被這一行替代了。

(3)第一次嘗試:freeze遷移

我們先試試freeze遷移,也就是ResNet18的任何一層的參數都固定,不參與反向傳播
這樣設置:

for param in model.parameters():  
    param.requires_grad = False # 默認為 True
# 顯然,這個 for 循環就是在對現在模型每一層説:“不要參與反向傳播”

(4)替換輸出頭以適配遷移目標任務

這一步的邏輯就是找到輸出層,替換輸出層。
要強調的一點是,這一步一定要在凍結之後進行
替換層級會讓層級初始化,並默認參與反向傳播。 如果你把凍結放在了這步之後,那整個網絡就不存在反向傳播了。
來看看具體怎麼做:

num_features = model.fc.in_features
# 這一行是在獲取模型的一個叫 fc 的層的輸入維度。
# 我們一般將網絡的最後一層全連接層命名為 fc
# 也就是説,這一行是在獲取模型的輸出層的輸入維度。  
model.fc = nn.Sequential(  
# Sequential 是 PyTorch 提供的一個容器類,它允許將多個層按順序組合在一起。
    nn.Linear(num_features, 1),  
    nn.Sigmoid()  
)
# 很明顯,我們把最後一層換成了只有一個神經元並經過Sigmoid激活來適配我們的任務要求。

我只是將原模型的輸出層更改成適配貓狗二分類的結構,如果你想在最後增加更多自己的層級設置,只需要在 Sequential 裏按順序添加,並確保層間維度匹配即可。

好了,現在我們就完成了所有配置,來看看效果吧。

2.1.3 第一次運行:freeze遷移+目標任務數據較多

回憶一下,我們的貓狗數據集總共只有 2400 幅圖像,這時一個相當小規模的數據集。在經過劃分後,用於訓練的數據就只有約 2000 個樣本,我們是這樣設置的:

train_size = int(0.8 * len(dataset))

我們先看看不改變這個劃分,只應用遷移學習的效果,結果如下:
image.png
好傢伙,強大無需多言,即使是隻訓練一輪的效果就已經強於我們之前的訓練效果了。
簡單分析一下原因:

  1. ImageNet 的超大規模樣本讓 ResNet18 模型學習了對通用特徵的識別。
  2. 卷積網絡和殘差網絡本身在圖學習的優越性也幫助了擬合。

簡單打個比方就是:教材好+學生聰明

2.1.4 第二次運行:freeze遷移+目標任務數據較少

之前我們在遷移學習的理論部分裏提到過,遷移學習的出現原因還是因此遷移目標任務的數據不足。
我們再次嚴格一下這個條件試試:

train_size = int(0.1 * len(dataset))

現在,訓練集,驗證集,測試集都只有 240 個樣本,我們再來看看效果:
image.png
可以看到和第一次運行的最大差別就在於最開始的輪次效果
換句話説,這一次我們只給模型提供了很少的“練習題”,而模型只能依靠最後一層去適應貓狗分類這個任務。

繼續打比方:這就像你找了一個學過大量數學知識的學霸來做一套小測驗,他的思維能力依然很強,但題目太少,所以在前面幾道題裏發揮得並不穩定。

不過隨着訓練輪次增加,曲線還是逐漸穩定下來,這説明遷移學習確實提供了很好的“啓動點”。
最終的結果也再次驗證了一個關鍵結論:數據越少,遷移學習越有價值。

如果我們不遷移、從頭訓練,那麼240 張訓練圖像幾乎不可能產生有意義的分類能力,而 freeze 遷移學習卻做到了“可用”。

現在,我們再來看看遷移學習的另一種形式。

2.1.5 第三次運行:fine-tuning

現在,我們再試試 fine-tuning 的效果,它是指在預訓練基礎上整體微調,也就是説,我們現在要”解凍“ 模型之前的層級。
如何解凍?你可能已經想到了:

for param in model.parameters():  
    param.requires_grad = False # 默認為 True

把這兩行刪掉或者註釋掉就OK了。
現在來看看運行結果,這裏我把訓練集佔比恢復到了0.8:
image.png
很顯然,效果仍然不錯,就不再詳細解釋了。

最後要説的是,遷移學習是我們在面對當前任務數據不足時的一種選擇,但它的效果不一定會像我們現在展示的這麼好。
雖然我們之前訓練貓狗二分類的結果一直不太好,但實際上是因為課程把卷積網絡安排在了後面介紹,我們之前使用全連接網絡訓練更像是"狗拿耗子",自然不會有很好的結果。
實際上,貓狗二分類只是一個圖學習中入門級的任務,加上和 ResNet18 的適配,所以在這次演示中起到了很好的效果,如果要應用遷移學習,還是要視具體任務選擇。
在調優過程中,不變的還是不斷的嘗試。

下面就來看看這周瞭解的另一種學習方式:多任務學習。

2.2 多任務學習

多任務學習在代碼邏輯上的重點在於數據集標籤和網絡結構上,其他部分不會產生太大的變化。
這部分就不再找專門的數據集來進行演示了,我們重點看看如何實現多任務學習的“前面共享、後面分頭”的結構
還是先拿我們之前的老結構拿出來曬曬:

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()  
  
    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

如果不使用這種一條路走到底的線性結構,而是實現允許“分叉” 的多任務學習樹形結構,就是這部分內容。

2.2.1 多任務學習的網絡結構

下面給出一個簡單的例子:
任務A:二分類(貓狗)
任務B:圖像亮度迴歸(0~1)
網絡結構就可以變成這樣:

class MultiTaskNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.relu = nn.ReLU()

        # —— 前面共享部分 ——
        self.shared1 = nn.Linear(128 * 128 * 3, 1024)
        self.shared2 = nn.Linear(1024, 256)

        # —— 後面分頭部分 ——
        # 任務 A:貓狗二分類
        self.headA = nn.Linear(256, 1)
        # 任務 B:亮度迴歸
        self.headB = nn.Linear(256, 1)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        x = self.flatten(x)
        x = self.relu(self.shared1(x))
        x = self.relu(self.shared2(x))
        outA = self.sigmoid(self.headA(x))     # 分類
        outB = self.headB(x)                   # 迴歸
        return outA, outB # 返回量增加為兩個

你可以看到,整個結構的前半部分的參數是共享的;後半部分兩個任務分頭走,最後的兩個返回值就是對兩個任務的預測。

2.2.2 多任務學習的損失函數怎麼寫?

因為現在有兩個輸出,就需要計算兩個任務的損失,再把它們加起來:

lossA = criterionA(outA, labelA)  # 比如二分類 BCE
lossB = criterionB(outB, labelB)  # 比如 MSE 迴歸損失
loss = lossA + lossB
loss.backward()

如果兩個任務重要程度不同,也可以加權:

loss = 0.7 * lossA + 0.3 * lossB

比如主任務是貓狗分類,輔助任務是亮度預測,那就讓“分類任務”權重大一點。

這就是本篇的全部內容了,下一章就到計算機視覺部分了,也就終於可以展開之前一直在提的卷積網絡了。

3.附錄

3.1 Pytorch版 遷移學習代碼

import torch  
import torch.nn as nn  
import torch.optim as optim  
from torchvision import datasets, transforms, models  
from torch.utils.data import DataLoader, random_split  
import matplotlib.pyplot as plt  
  
transform = transforms.Compose([  
    transforms.Resize((224, 224)),     # ResNet 輸入 224x224    transforms.ToTensor(),  
    transforms.Normalize([0.485, 0.456, 0.406],  
                         [0.229, 0.224, 0.225])   # ResNet 官方均值方差  
])  
  
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)  
  
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  
  
model = models.resnet18(pretrained=True)  
  
# 是否凍結預訓練參數  
for param in model.parameters():  
    param.requires_grad = False  
  
# 替換最後一層  
num_features = model.fc.in_features  
model.fc = nn.Sequential(  
    nn.Linear(num_features, 1),  
    nn.Sigmoid()  
)  
  
model = model.to(device)  
  
criterion = nn.BCELoss()  
optimizer = optim.Adam(model.fc.parameters(), lr=0.001)  
  
epochs = 10  
train_losses = []  
train_accs = []  
val_accs = []  
  
for epoch in range(epochs):  
    model.train()  
    epoch_train_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_train_loss += loss.item()  
        preds = (outputs > 0.5).int()  
        correct_train += (preds == labels.int()).sum().item()  
        total_train += labels.size(0)  
  
    avg_train_loss = epoch_train_loss / len(train_loader)  
    train_acc = correct_train / total_train  
  
    train_losses.append(avg_train_loss)  
    train_accs.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_accs.append(val_acc)  
  
    print(f"輪次 [{epoch+1}/{epochs}]  "  
          f"訓練損失: {avg_train_loss:.4f}  "  
          f"訓練準確率: {train_acc:.4f}  "  
          f"驗證準確率: {val_acc:.4f}")  
  
  
plt.rcParams['font.sans-serif'] = ['SimHei']  
plt.rcParams['axes.unicode_minus'] = False  
plt.figure(figsize=(10,5))  
plt.plot(train_losses, label='訓練損失')  
plt.plot(train_accs, label='訓練準確率')  
plt.plot(val_accs, label='驗證準確率')  
plt.legend()  
plt.grid(True)  
plt.show()  
  
  
model.eval()  
correct = 0  
total = 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 += (preds == labels.int()).sum().item()  
        total += labels.size(0)  
  
print(f"測試準確率: {correct / total:.4f}")

3.2 Tensorflow版 遷移學習代碼

注:TF沒有內置ResNet的18版本,而是ResNet50以及更高的版本,而TF和提供第三方 ResNet18 庫的兼容性又不太好,因此這裏實際上使用的是ResNet50

import tensorflow as tf  
from tensorflow import keras  
from tensorflow.keras import layers, models  
import matplotlib.pyplot as plt  
  
IMG_SIZE = (224, 224)  
BATCH_SIZE = 32  
train_ds = 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 = 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_ds = val_test_ds.take(len(val_test_ds) // 2)  
test_ds = val_test_ds.skip(len(val_test_ds) // 2)  
  
preprocess = keras.applications.resnet50.preprocess_input  
  
def preprocess_fn(image, label):  
    return preprocess(tf.cast(image, tf.float32)), tf.cast(label, tf.float32)  
  
train_ds = train_ds.map(preprocess_fn)  
val_ds = val_ds.map(preprocess_fn)  
test_ds = test_ds.map(preprocess_fn)  
  
train_ds = train_ds.prefetch(tf.data.AUTOTUNE)  
val_ds = val_ds.prefetch(tf.data.AUTOTUNE)  
test_ds = test_ds.prefetch(tf.data.AUTOTUNE)  
  

# 構建遷移學習模型(凍結 ResNet50)  
base_model = keras.applications.ResNet50(  
    weights="imagenet",  
    include_top=False,  
    input_shape=(224, 224, 3),  
    pooling="avg"  
)  
  
base_model.trainable = False   # 凍結  
  
inputs = keras.Input(shape=(224, 224, 3))  
x = base_model(inputs, training=False)  
outputs = layers.Dense(1, activation="sigmoid")(x)  
model = keras.Model(inputs, outputs)  
  
model.compile(  
    optimizer=keras.optimizers.Adam(learning_rate=0.001),  
    loss="binary_crossentropy",  
    metrics=["accuracy"]  
)  
  

# 訓練  
history = model.fit(  
    train_ds,  
    validation_data=val_ds,  
    epochs=10  
)  
  
plt.figure(figsize=(10, 5))  
plt.plot(history.history["loss"], label="訓練損失")  
plt.plot(history.history["accuracy"], label="訓練準確率")  
plt.plot(history.history["val_accuracy"], label="驗證準確率")  
plt.legend()  
plt.grid(True)  
plt.show()  
  
test_loss, test_acc = model.evaluate(test_ds)  
print("測試準確率:", test_acc)

3.3 Tensorflow版 多任務學習網絡結構

class MultiTaskNet(tf.keras.Model):
    def __init__(self):
        super(MultiTaskNet, self).__init__()
        # —— 前面共享部分 ——
        self.flatten = layers.Flatten()
        self.shared1 = layers.Dense(1024, activation='relu')
        self.shared2 = layers.Dense(256, activation='relu')
        # —— 後面分頭部分 ——
        # 任務 A:貓狗二分類
        self.headA = layers.Dense(1, activation='sigmoid')
        # 任務 B:亮度迴歸
        self.headB = layers.Dense(1, activation=None)

    def call(self, inputs, training=False):
        x = self.flatten(inputs)
        x = self.shared1(x)
        x = self.shared2(x)
        outA = self.headA(x)   # 分類輸出
        outB = self.headB(x)   # 迴歸輸出
        return outA, outB
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.