在深度學習中梯度訊息扮演著關鍵的角色,尤其在神經網路的訓練過程中。梯度代表了函數相對於參數的變化率,它告訴我們如何調整神經網路的權重和參數,以最小化或最大化一個特定的損失函數。因此梯度下降法是優化神經網路的主要手段之一,它依賴梯度訊息來更新模型的參數,使其更好地適應訓練數據。
然而今天要談的是利用梯度訊息解釋神經網路,也就是當訓練一個神經網路後,透過計算神經網路對於輸入影像的梯度,我們就可以了解每個輸入特徵(例如像素)對於神經網路輸出的影響程度,並解釋神經網路的決策過程。以下是一些相關文獻,可以使用梯度訊息來表示輸入影像中每個特徵的重要度:
Attribution
(歸因)是一個用於機器學習和深度學習領域的術語,它指的是試圖理解機器學習模型如何做出特定預測或決策的過程。以下使用一個簡單的線性迴歸例子,解釋 attribution 的概念。假設有兩個特徵會影響模型最終的輸出結果,那我想知道這兩個特徵的影響性分別有多大。通常可以使用偏微分(Partial Derivatives)來進行分析。這是數學上的一個方法,可以計算某個函數對於其中一個變數的偏導數,以評估該變數對函數值的影響。
我們可以用一個簡單的例子來解釋如何計算特徵對模型輸出的影響程度。假設我們有一個簡單的線性模型如下:
這個模型有兩個特徵,x1 和 x2,它們分別乘以不同的權重2和3。現在我們想知道 x1 和 x2 對 y 的影響程度。
首先,我們計算對x1的偏導數:
∂y/∂x1 = 2
這表示當 x1 增加1個單位時,y 會增加2個單位。所以,x1對 y 的影響是正向的,並且影響程度為2。
接下來,我們計算對 x2 的偏導數:
∂y/∂x2 = 3
這表示當 x2 增加1個單位時,y 會增加3個單位。所以,x2 對 y 的影響也是正向的,並且影響程度為3。
根據這個例子,我們可以得出結論,x2 對 y 的影響程度比 x1 大,因為 ∂y/∂x2
的絕對值大於 ∂y/∂x1
的絕對值。這是一個極簡單的例子,實際上的模型可能更複雜,但這個方法可以用來評估不同特徵對模型輸出的相對影響程度。此外這個方法假設模型是線性的,並且特徵之間不存在交互作用,對於複雜的非線性模型,可以使用一階泰勒展開式把它近似成線性模型。
這裡介紹一個簡單的 Gradient-Based 方法,就是 Image-Specific Class Saliency(圖像特定類別顯著性),也有些文獻稱它 Sensitivity Analysis (敏感度分析)。可以藉由計算輸入圖像的每個 pixel 的梯度,獲取 Saliency Map。整體步驟流程如下:
對數學與推導過程有興趣的讀者可以參考以下這份電子書。在文中作者稱上述方法為 Vanilla Gradient。
在今天的範例中所使用的預訓練模型為 ResNet50。使用的方式跟昨天幾乎一模一樣,都是採用 tf.keras.applications
內建所提供的模型參數。ResNet50 突破了深度神經網路 Deep 的受限,引入了殘差學習的概念。通常隨著神經網路層數的增加,模型的性能會變得更好,但在一定深度後,模型會遭遇梯度消失或梯度爆炸的問題,導致訓練變得困難。因此 ResNet 引入 Residual Block(殘差模塊)允許訊息在不同層之間跳躍(即跳過某些層),進而解決了這個問題。
ResNet 論文: Deep Residual Learning for Image Recognition (CVPR 2016 best paper)
首先使用 TensorFlow 載入 ResNet50 模型,將輸入張量(tensor)連接到預訓練的神經網路層,imagenet
表示使用在 ImageNet 資料集上預訓練的權重。include_top=True
表示輸出包括模型的最後分類層(全連接層),此模型通常用於影像分類任務。
from tensorflow.keras.applications.resnet50 import ResNet50
from tensorflow.keras.layers import Input
# 建立一個輸入張量,指定圖像大小為224x224(RGB色彩通道)
input_tensor = Input(shape=(224, 224, 3))
# 建立 ResNet50 模型
model = ResNet50(input_tensor=input_tensor, weights='imagenet', include_top=True)
接著載入一張圖像,對其進行預處理。其中 np.expand_dims()
的目的是將圖像轉換為模型可接受的維度,這裡將圖像包裝在一個批次(batch)中,通常是一個批次只有一張圖像。最後使用 ResNet50 模型的預處理函數 preprocess_input()
來處理圖像,以確保圖像的數值範圍和格式符合模型的要求。
import numpy as np
import tensorflow as tf
from tensorflow.keras.applications.resnet50 import preprocess_input
# 載入圖像
image = tf.keras.utils.load_img('./dataset/cat_dog.jpg')
image = tf.keras.utils.img_to_array(image) # 將載入的圖像轉換為數組形式
x = np.expand_dims(image.copy(), axis=0) # 將圖像轉換為模型可接受的維度
# 預處理圖像
x = preprocess_input(x)
確認輸入影像都完成處理過後,就可以使用已建立的 ResNet50 模型進行圖像分類預測,返回分類機率。最後再使用 decode_predictions()
解析取得預測結果,並取得類別名稱和相對應的預測機率。pred_class_idx
則是預測的標籤索引,最後模型解釋會需要用到它。
from tensorflow.keras.applications.resnet50 import decode_predictions
# 進行圖像分類預測
pred_proba = model.predict(x) # 返回分類機率
# 解析預測結果
pred_class_idx = pred_proba.argmax(axis=1)[0] # 找到具有最高機率的類別索引
pred_class = decode_predictions(pred_proba, top=1)[0][0] # 解析取得預測結果
我們先來看看模型預測的結果。雖然這張影像同時有一隻貓和狗,但模型在神經網路中先抓取到狗的重要特徵(例如:鼻子、嘴巴),因此最終模型預測 bull_mastiff
(鬥牛獒),該類別的機率值有 60%。有興趣的讀者,也可以跟昨天的模型進行比較。
import matplotlib.pylab as plt
plt.imshow(image.astype('uint8'))
plt.axis('off')
predicted_class_name = pred_class[1]
_ = plt.title(f"Prediction: {predicted_class_name} {pred_class[2]:.2f}")
首先我們將此演算法包裝成一個函式方便呼叫。這一個函式主要接受兩個參數:
其中為了要進行梯度的計算,我們需要將輸入的圖片從 numpy array 轉換成 TensorFlow Tensor 格式。最後的 return 是計算目標類別機率 probs
對於輸入圖像 image
的梯度。tape.gradient
函數接受兩個參數,分別是梯度的目標和原影像,這裡我們希望計算目標是 probs
相對於輸入影像 image
。
def compute_gradients(image, target_class_idx):
image = tf.convert_to_tensor(image) # 將輸入的影像轉換為 TensorFlow 張量
with tf.GradientTape() as tape: # 用於計算梯度
tape.watch(image) # 監控圖像,以便計算其梯度
predictions = model(image) # 使用模型進行預測
probs = predictions[:, target_class_idx] # 取得目標類別的預測機率
return tape.gradient(probs, image) # 計算機率對於影像的梯度並返回
接著我們馬上將剛剛測試的 RestNet50 預測鬥牛獒的結果放進去此函式進行解釋。這段程式碼的目的是計算梯度並生成一個歸因遮罩(attribution mask)。首先呼叫 compute_gradients 函式,並傳入一個影像 x 和目標類別的索引 pred_class_idx。該函式會計算影像對於目標類別的梯度,並將結果儲存在 gradients 變數中。 接著使用 tf.squeeze 函式將 gradients 張量的多餘維度去除,然後使用 tf.math.abs 函式計算絕對值。這樣做是為了確保梯度的正負號不影響歸因遮罩的生成。最後使用 tf.reduce_sum 函式將計算結果沿著最後一個維度(通道軸)相加,生成最終的歸因遮罩,並將結果儲存在 attribution_mask 變數中。
# 計算機率對於影像的梯度
gradients = compute_gradients(
image=x,
target_class_idx=pred_class_idx)
# 產生可解釋的遮罩
attribution_mask = tf.reduce_sum(tf.math.abs(tf.squeeze(gradients)), axis=-1)
最後讓我們瞧瞧結果。
import matplotlib.cm as cm
fig, axs = plt.subplots(nrows=1, ncols=3, squeeze=False, figsize=(16, 8),
subplot_kw={'xticks':[], 'yticks':[]})
axs[0, 0].set_title('Original image', fontsize=20)
axs[0, 0].imshow(image/255)
axs[0, 1].set_title('Attribution Mask', fontsize=20)
axs[0, 1].imshow(attribution_mask, cmap=cm.inferno)
axs[0, 2].set_title('Overlay', fontsize=20)
axs[0, 2].imshow(attribution_mask, cmap=cm.inferno)
axs[0, 2].imshow(image/255, alpha=0.4)
本系列教學內容及範例程式都可以從我的 GitHub 取得!