![]() |
![]() |
![]() |
![]() |
![]() |
本教學課程示範如何實作整合式梯度 (IG),這是一種 可解釋 AI 技術,於論文 Deep Networks 的公理歸因 中提出。IG 旨在根據模型的特徵解釋模型預測之間的關係。它有許多用途,包括瞭解特徵重要性、識別資料偏誤和偵錯模型效能。
IG 已成為一種熱門的可解釋性技術,因為它廣泛適用於任何可微分模型 (例如圖像、文字、結構化資料)、易於實作、理論基礎充分,以及相較於其他替代方法在運算上更有效率,使其能夠擴展到大型網路和特徵空間 (例如圖像)。
在本教學課程中,您將逐步完成 IG 的實作,以瞭解圖像分類器的像素特徵重要性。以這張噴射水柱的消防船 圖像 為例。您會將這張圖像分類為消防船,並可能醒目提示構成船隻和水砲的像素,因為它們對您的決策很重要。您的模型稍後也會在本教學課程中將此圖像分類為消防船;但是,在解釋其決策時,它是否醒目提示相同的像素很重要?
在下方標題為「IG 歸因遮罩」和「原始圖像 + IG 遮罩疊加」的圖像中,您可以看到您的模型反而醒目提示 (以紫色顯示) 構成船隻水砲和水柱的像素,因為它們對其決策的重要性高於船隻本身。您的模型將如何推廣到新的消防船?沒有水柱的消防船呢?請繼續閱讀以深入瞭解 IG 的運作方式,以及如何將 IG 應用於您的模型,以更深入瞭解其預測與基礎特徵之間的關係。
設定
import matplotlib.pylab as plt
import numpy as np
import tensorflow as tf
import tensorflow_hub as hub
從 TF-Hub 下載預先訓練的圖像分類器
IG 可以應用於任何可微分模型。本著原始論文的精神,您將使用相同模型 Inception V1 的預先訓練版本,您將從 TensorFlow Hub 下載。
model = tf.keras.Sequential([
hub.KerasLayer(
name='inception_v1',
handle='https://tfhub.dev/google/imagenet/inception_v1/classification/4',
trainable=False),
])
model.build([None, 224, 224, 3])
model.summary()
從模組頁面中,您需要記住以下關於 Inception V1 的資訊
輸入:模型的預期輸入形狀為 (None, 224, 224, 3)
。這是一個 dtype 為 float32 且形狀為 (batch_size, height, width, RGB channels)
的密集 4D 張量,其元素是像素的 RGB 色彩值,已正規化為範圍 [0, 1]。第一個元素為 None
,表示模型可以接受任何整數批次大小。
輸出:形狀為 (batch_size, 1001)
的對數機率 tf.Tensor
。每一列代表模型針對 ImageNet 中 1,001 個類別的預測分數。對於模型的頂端預測類別索引,您可以使用 tf.math.argmax(predictions, axis=-1)
。此外,您也可以使用 tf.nn.softmax(predictions, axis=-1)
將模型的對數機率輸出轉換為所有類別的預測機率,以量化模型的不確定性,並探索用於偵錯的類似預測類別。
def load_imagenet_labels(file_path):
labels_file = tf.keras.utils.get_file('ImageNetLabels.txt', file_path)
with open(labels_file) as reader:
f = reader.read()
labels = f.splitlines()
return np.array(labels)
imagenet_labels = load_imagenet_labels('https://storage.googleapis.com/download.tensorflow.org/data/ImageNetLabels.txt')
使用 tf.image
載入和預先處理圖像
您將使用來自 維基共享資源 的兩張圖像來說明 IG:消防船 和 大貓熊。
def read_image(file_name):
image = tf.io.read_file(file_name)
image = tf.io.decode_jpeg(image, channels=3)
image = tf.image.convert_image_dtype(image, tf.float32)
image = tf.image.resize_with_pad(image, target_height=224, target_width=224)
return image
img_url = {
'Fireboat': 'http://storage.googleapis.com/download.tensorflow.org/example_images/San_Francisco_fireboat_showing_off.jpg',
'Giant Panda': 'http://storage.googleapis.com/download.tensorflow.org/example_images/Giant_Panda_2.jpeg',
}
img_paths = {name: tf.keras.utils.get_file(name, url) for (name, url) in img_url.items()}
img_name_tensors = {name: read_image(img_path) for (name, img_path) in img_paths.items()}
plt.figure(figsize=(8, 8))
for n, (name, img_tensors) in enumerate(img_name_tensors.items()):
ax = plt.subplot(1, 2, n+1)
ax.imshow(img_tensors)
ax.set_title(name)
ax.axis('off')
plt.tight_layout()
分類圖像
首先對這些圖像進行分類,並顯示前 3 個最可靠的預測。以下是一個公用程式函式,用於擷取前 k 個預測標籤和機率。
def top_k_predictions(img, k=3):
image_batch = tf.expand_dims(img, 0)
predictions = model(image_batch)
probs = tf.nn.softmax(predictions, axis=-1)
top_probs, top_idxs = tf.math.top_k(input=probs, k=k)
top_labels = imagenet_labels[tuple(top_idxs)]
return top_labels, top_probs[0]
for (name, img_tensor) in img_name_tensors.items():
plt.imshow(img_tensor)
plt.title(name, fontweight='bold')
plt.axis('off')
plt.show()
pred_label, pred_prob = top_k_predictions(img_tensor)
for label, prob in zip(pred_label, pred_prob):
print(f'{label}: {prob:0.1%}')
計算整合式梯度
您的模型 Inception V1 是一個已學習的函式,用於描述輸入特徵空間 (圖像像素值) 與輸出空間 (由介於 0 和 1 之間的 ImageNet 類別機率值定義) 之間的對應關係。神經網路的早期可解釋性方法使用梯度來指派特徵重要性分數,梯度會告訴您哪些像素在模型預測函式上給定點的模型預測方面具有最陡峭的局部相對關係。但是,梯度僅描述模型預測函式相對於像素值的局部變更,並未完整描述您的整個模型預測函式。當您的模型完整「學習」個別像素範圍與正確 ImageNet 類別之間的關係時,此像素的梯度將會飽和,表示變得越來越小,甚至趨近於零。請考量以下簡單的模型函式
def f(x):
"""A simplified model function."""
return tf.where(x < 0.8, x, 0.8)
def interpolated_path(x):
"""A straight line path."""
return tf.zeros_like(x)
x = tf.linspace(start=0.0, stop=1.0, num=6)
y = f(x)
左圖:像素
x
的模型梯度在 0.0 到 0.8 之間為正值,但在 0.8 到 1.0 之間變為 0.0。像素x
顯然對將您的模型推向真實類別的 80% 預測機率具有重大影響。像素x
的重要性很小或不連續是否合理?右圖:IG 背後的直覺是累積像素
x
的局部梯度,並將其重要性歸因為一個分數,以衡量它對模型整體輸出類別機率的增加或減少程度。您可以將 IG 分解並分為 3 個部分來計算- 沿著特徵空間中的一條直線在 0 (基準或起點) 和 1 (輸入像素值) 之間內插小步驟
- 計算模型預測在每個步驟之間相對於每個步驟的梯度
- 透過累積 (累積平均值) 這些局部梯度,近似基準和輸入之間的積分。
為了加強這種直覺,您將逐步完成這 3 個部分,方法是將 IG 應用於以下「消防船」圖像範例。
建立基準
基準是一個輸入圖像,用作計算特徵重要性的起點。直覺上,您可以將基準的解釋角色視為代表每個像素在「消防船」預測中不存在時的影響,以與每個像素在輸入圖像中存在時對「消防船」預測的影響形成對比。因此,基準的選擇在解釋和視覺化像素特徵重要性方面發揮著核心作用。如需關於基準選擇的其他討論,請參閱本教學課程底部「後續步驟」章節中的資源。在這裡,您將使用像素值均為零的黑色圖像。
您可以實驗的其他選擇包括全白圖像或隨機圖像,您可以使用 tf.random.uniform(shape=(224,224,3), minval=0.0, maxval=1.0)
建立。
baseline = tf.zeros(shape=(224,224,3))
plt.imshow(baseline)
plt.title("Baseline")
plt.axis('off')
plt.show()
將公式解壓縮為程式碼
整合式梯度的公式如下
\(IntegratedGradients_{i}(x) ::= (x_{i} - x'_{i})\times\int_{\alpha=0}^1\frac{\partial F(x'+\alpha \times (x - x'))}{\partial x_i}{d\alpha}\)
其中
\(_{i}\) = 特徵
\(x\) = 輸入
\(x'\) = 基準
\(\alpha\) = 用於擾動特徵的內插常數
實際上,計算定積分在數值上並非始終可行,而且在運算上可能成本高昂,因此您會計算以下數值近似值
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\partial F(x' + \frac{k}{m}\times(x - x'))}{\partial x_{i} } \times \frac{1}{m}\)
其中
\(_{i}\) = 特徵 (個別像素)
\(x\) = 輸入 (圖像張量)
\(x'\) = 基準 (圖像張量)
\(k\) = 縮放特徵擾動常數
\(m\) = Riemann 和近似積分中的步驟數
\((x_{i}-x'_{i})\) = 基準差異的項。這對於縮放整合式梯度並將其保持在原始圖像的單位中是必要的。從基準圖像到輸入的路徑位於像素空間中。由於使用 IG 時,您正在沿直線 (線性轉換) 積分,因此這最終大致相當於內插圖像函式的導數相對於 \(\alpha\) 的積分項,並具有足夠的步驟。積分會將每個像素的梯度乘以沿路徑的像素變化量。將此積分實作為從一個圖像到另一個圖像的均勻步驟更簡單,方法是替換 \(x := (x' + \alpha(x-x'))\)。因此,變數的變化給出 \(dx = (x-x')d\alpha\)。\((x-x')\) 項是常數,並且從積分中分解出來。
內插圖像
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\partial F(\overbrace{x' + \frac{k}{m}\times(x - x')}^\text{在 k 間隔內內插 m 個圖像})}{\partial x_{i} } \times \frac{1}{m}\)
首先,您將在基準圖像和原始圖像之間產生 線性內插。您可以將內插圖像視為基準圖像和輸入之間特徵空間中的小步驟,以原始方程式中的 \(\alpha\) 表示。
m_steps=50
alphas = tf.linspace(start=0.0, stop=1.0, num=m_steps+1) # Generate m_steps intervals for integral_approximation() below.
def interpolate_images(baseline,
image,
alphas):
alphas_x = alphas[:, tf.newaxis, tf.newaxis, tf.newaxis]
baseline_x = tf.expand_dims(baseline, axis=0)
input_x = tf.expand_dims(image, axis=0)
delta = input_x - baseline_x
images = baseline_x + alphas_x * delta
return images
使用上述函式在黑色基準圖像和「消防船」範例圖像之間以 alpha 間隔沿線性路徑產生內插圖像。
interpolated_images = interpolate_images(
baseline=baseline,
image=img_name_tensors['Fireboat'],
alphas=alphas)
視覺化內插圖像。注意:思考 \(\alpha\) 常數的另一種方式是,它會持續增加每個內插圖像的強度。
fig = plt.figure(figsize=(20, 20))
i = 0
for alpha, image in zip(alphas[0::10], interpolated_images[0::10]):
i += 1
plt.subplot(1, len(alphas[0::10]), i)
plt.title(f'alpha: {alpha:.1f}')
plt.imshow(image)
plt.axis('off')
plt.tight_layout();
計算梯度
本節說明如何計算梯度,以衡量特徵變更與模型預測變更之間的關係。就圖像而言,梯度會告訴我們哪些像素對模型預測的類別機率具有最強烈的影響。
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times\sum_{k=1}^{m}\frac{\overbrace{\partial F(\text{內插圖像})}^\text{計算梯度} }{\partial x_{i} } \times \frac{1}{m}\)
其中
\(F()\) = 您的模型預測函式
\(\frac{\partial{F} }{\partial{x_i} }\) = 相對於每個特徵 \(x_i\) 的模型 F 預測函式的梯度 (偏導數向量 \(\partial\))
TensorFlow 使用 tf.GradientTape
讓您輕鬆計算梯度。
def compute_gradients(images, target_class_idx):
with tf.GradientTape() as tape:
tape.watch(images)
logits = model(images)
probs = tf.nn.softmax(logits, axis=-1)[:, target_class_idx]
return tape.gradient(probs, images)
計算內插路徑上每個圖像相對於正確輸出的梯度。回想一下,您的模型會傳回形狀為 (1, 1001)
的 Tensor
,其中包含您轉換為每個類別預測機率的對數機率。您需要將正確的 ImageNet 目標類別索引傳遞至圖像的 compute_gradients
函式。
path_gradients = compute_gradients(
images=interpolated_images,
target_class_idx=555)
請注意 (n_interpolated_images, img_height, img_width, RGB)
的輸出形狀,它為我們提供了內插路徑上每個圖像的每個像素的梯度。您可以將這些梯度視為衡量模型預測在特徵空間中每個小步驟的變更。
print(path_gradients.shape)
視覺化梯度飽和度
回想一下,您剛才計算的梯度描述了模型預測「消防船」機率的局部變更,並且可能會飽和。
這些概念使用您在上方計算的梯度在下方 2 個繪圖中視覺化。
pred = model(interpolated_images)
pred_proba = tf.nn.softmax(pred, axis=-1)[:, 555]
左圖:此繪圖顯示您的模型對「消防船」類別的信賴度如何隨 alpha 變化。請注意,梯度或線條的斜率在 0.6 到 1.0 之間大致趨於平坦或飽和,然後穩定在最終的「消防船」預測機率約 40%。
右圖:右圖更直接地顯示 alpha 上的平均梯度量級。請注意這些值如何急劇接近,甚至短暫地降至零以下。事實上,您的模型從 alpha 值較低的梯度中「學習」最多,然後才會飽和。直覺上,您可以將此視為您的模型已學習像素 (例如水砲) 以做出正確的預測,將這些像素梯度降至零,但當 alpha 值接近原始輸入圖像時,仍然非常不確定且專注於虛假的橋樑或水柱像素。
為了確保這些重要的水砲像素反映為對「消防船」預測很重要,您將繼續在下方學習如何累積這些梯度,以準確近似每個像素如何影響您的「消防船」預測機率。
累積梯度 (積分近似值)
有許多不同的方法可以計算 IG 積分的數值近似值,這些方法在不同函式的準確性和收斂性之間具有不同的權衡。一種熱門的方法類別稱為 Riemann 和。在這裡,您將使用梯形規則 (您可以在本教學課程末尾找到其他程式碼,以探索不同的近似方法)。
\(IntegratedGrads^{approx}_{i}(x)::=(x_{i}-x'_{i})\times \overbrace{\sum_{k=1}^{m} }^\text{加總 m 個局部梯度}\text{gradients(內插圖像)} \times \overbrace{\frac{1}{m} }^\text{除以 m 個步驟}\)
從方程式中,您可以看到您正在加總 m
個梯度並除以 m
個步驟。您可以將這兩個運算一起實作,作為第 3 部分,即m
個內插預測和輸入圖像的局部梯度的平均值。
def integral_approximation(gradients):
# riemann_trapezoidal
grads = (gradients[:-1] + gradients[1:]) / tf.constant(2.0)
integrated_gradients = tf.math.reduce_mean(grads, axis=0)
return integrated_gradients
integral_approximation
函式採用目標類別的預測機率相對於基準圖像和原始圖像之間的內插圖像的梯度。
ig = integral_approximation(
gradients=path_gradients)
您可以確認跨 m
個內插圖像的梯度取平均值會傳回與原始「大貓熊」圖像形狀相同的整合式梯度張量。
print(ig.shape)
整合在一起
現在,您將把先前的 3 個一般部分組合在一起,放入 IntegratedGradients
函式中,並使用 @tf.function 裝飾器將其編譯為高效能的可呼叫 TensorFlow 圖形。這在下方實作為 5 個較小的步驟
\(IntegratedGrads^{approx}_{i}(x)::=\overbrace{(x_{i}-x'_{i})}^\text{5.}\times \overbrace{\sum_{k=1}^{m} }^\text{4.} \frac{\partial \overbrace{F(\overbrace{x' + \overbrace{\frac{k}{m} }^\text{1.}\times(x - x'))}^\text{2.} }^\text{3.} }{\partial x_{i} } \times \overbrace{\frac{1}{m} }^\text{4.}\)
產生 alpha \(\alpha\)
產生內插圖像 = \((x' + \frac{k}{m}\times(x - x'))\)
計算模型 \(F\) 輸出預測與輸入特徵之間的梯度 = \(\frac{\partial F(\text{內插路徑輸入})}{\partial x_{i} }\)
透過平均梯度進行積分近似值 = \(\sum_{k=1}^m \text{gradients} \times \frac{1}{m}\)
相對於原始圖像縮放整合式梯度 = \((x_{i}-x'_{i}) \times \text{整合式梯度}\)。此步驟之所以必要,是為了確保跨多個內插圖像累積的歸因值採用相同的單位,並忠實地表示原始圖像上的像素重要性。
def integrated_gradients(baseline,
image,
target_class_idx,
m_steps=50,
batch_size=32):
# Generate alphas.
alphas = tf.linspace(start=0.0, stop=1.0, num=m_steps+1)
# Collect gradients.
gradient_batches = []
# Iterate alphas range and batch computation for speed, memory efficiency, and scaling to larger m_steps.
for alpha in tf.range(0, len(alphas), batch_size):
from_ = alpha
to = tf.minimum(from_ + batch_size, len(alphas))
alpha_batch = alphas[from_:to]
gradient_batch = one_batch(baseline, image, alpha_batch, target_class_idx)
gradient_batches.append(gradient_batch)
# Concatenate path gradients together row-wise into single tensor.
total_gradients = tf.concat(gradient_batches, axis=0)
# Integral approximation through averaging gradients.
avg_gradients = integral_approximation(gradients=total_gradients)
# Scale integrated gradients with respect to input.
integrated_gradients = (image - baseline) * avg_gradients
return integrated_gradients
@tf.function
def one_batch(baseline, image, alpha_batch, target_class_idx):
# Generate interpolated inputs between baseline and input.
interpolated_path_input_batch = interpolate_images(baseline=baseline,
image=image,
alphas=alpha_batch)
# Compute gradients between model outputs and interpolated inputs.
gradient_batch = compute_gradients(images=interpolated_path_input_batch,
target_class_idx=target_class_idx)
return gradient_batch
ig_attributions = integrated_gradients(baseline=baseline,
image=img_name_tensors['Fireboat'],
target_class_idx=555,
m_steps=240)
同樣地,您可以檢查 IG 特徵歸因是否與輸入「消防船」圖像具有相同的形狀。
print(ig_attributions.shape)
論文建議步驟數範圍介於 20 到 300 之間,具體取決於範例 (儘管實際上可能更高,達到 1,000 以上,才能準確近似積分)。您可以在本教學課程末尾的「後續步驟」資源中找到其他程式碼,以檢查適當的步驟數。
視覺化歸因
您已準備好視覺化歸因,並將其疊加在原始圖像上。以下程式碼會加總跨色彩通道的整合式梯度絕對值,以產生歸因遮罩。此繪圖方法捕捉像素對模型預測的相對影響。
查看「消防船」圖像上的歸因,您可以看到模型識別出水砲和噴嘴有助於其正確預測。
_ = plot_img_attributions(image=img_name_tensors['Fireboat'],
baseline=baseline,
target_class_idx=555,
m_steps=240,
cmap=plt.cm.inferno,
overlay_alpha=0.4)
在「大貓熊」圖像上,歸因醒目提示了大貓熊臉部的紋理、鼻子和毛皮。
_ = plot_img_attributions(image=img_name_tensors['Giant Panda'],
baseline=baseline,
target_class_idx=389,
m_steps=55,
cmap=plt.cm.viridis,
overlay_alpha=0.5)
用途與限制
使用案例
- 在部署模型之前採用整合式梯度等技術,可以協助您培養對模型運作方式和原因的直覺。此技術醒目提示的特徵是否符合您的直覺?如果沒有,則可能表示您的模型或資料集存在錯誤,或是過度擬合。
限制
整合式梯度技術提供個別範例的特徵重要性。但是,它不提供整個資料集的整體特徵重要性。
整合式梯度技術提供個別特徵重要性,但不解釋特徵互動和組合。
後續步驟
本教學課程介紹了整合式梯度的基本實作。作為後續步驟,您可以使用此筆記本自行嘗試將此技術應用於不同的模型和圖像。
對於感興趣的讀者,本教學課程有一個較長的版本 (其中包含用於不同基準、計算積分近似值以及判斷足夠步驟數的程式碼),您可以在 這裡找到。
為了加深您的理解,請查看論文 Deep Networks 的公理歸因 和 Github 存放區,其中包含先前 TensorFlow 版本的實作。您也可以在 distill.pub 上探索特徵歸因以及不同基準的影響。
有興趣將 IG 納入您的生產環境機器學習工作流程,以用於特徵重要性、模型錯誤分析和資料偏誤監控嗎?請查看 Google Cloud 的 可解釋 AI 產品,該產品支援 IG 歸因。Google AI PAIR 研究團隊也開放原始碼 What-if 工具,可用於模型偵錯,包括視覺化 IG 特徵歸因。