Keras集成梯度用於模型可解釋性

【導讀】集成梯度是一種用於將分類模型的預測歸因於其輸入特徵的技術。這是一種模型可解釋性技術:它來可視化輸入要素和模型預測之間的關係。

原文連結:

https://keras.io/examples/vision/integrated_gradients/

介紹

集成梯度是在計算預測輸出相對於輸入特徵的梯度時的變體。要計算集成梯度,我們需要執行以下步驟:

1. 識別輸入和輸出。在我們的例子中,輸入是圖像,輸出是模型的最後一層(具有softmax激活的Dense層)。

2.在對特定數據點進行預測時,計算哪些特徵對神經網絡很重要。為了識別這些特徵,我們需要選擇一個基線輸入。基線輸入可以是黑色圖像(所有像素值均設置為零)或隨機噪聲。基線輸入的形狀需要與我們的輸入圖像相同,例如(299,299,3)。

3. 在給定的步驟數內插基線。步數表示對於給定的輸入圖像,我們需要進行梯度近似的步驟。步驟數是一個超參數。作者建議使用20到1000步之間的任何步長。

4. 預處理這些插值圖像並進行正向傳遞。

5. 獲取這些插值圖像的梯度。

6. 使用梯形法則近似梯度積分。

設置

import numpy as npimport matplotlib.pyplot as pltfrom scipy import ndimagefrom IPython.display import Image
import tensorflow as tffrom tensorflow import kerasfrom tensorflow.keras import layersfrom tensorflow.keras.applications import xception
# Size of the input imageimg_size = (299, 299, 3)
# Load Xception model with imagenet weightsmodel = xception.Xception(weights="imagenet")
# The local path to our target imageimg_path = keras.utils.get_file("elephant.jpg", "https://i.imgur.com/Bvro0YD.png")display(Image(img_path))

顯示圖片

集成梯度算法

集成梯度算法

def get_img_array(img_path, size=(299, 299)):    # `img` is a PIL image of size 299x299img = keras.preprocessing.image.load_img(img_path, target_size=size)    # `array` is a float32 Numpy array of shape (299, 299, 3)array = keras.preprocessing.image.img_to_array(img)    # We add a dimension to transform our array into a "batch"    # of size (1, 299, 299, 3)array = np.expand_dims(array, axis=0)return array
def get_gradients(img_input, top_pred_idx):"""Computes the gradients of outputs w.r.t input image.
Args:img_input: 4D image tensortop_pred_idx: Predicted label for the input image
Returns:Gradients of the predictions w.r.t img_input"""images = tf.cast(img_input, tf.float32)
with tf.GradientTape() as tape:tape.watch(images)preds = model(images)top_class = preds[:, top_pred_idx]
grads = tape.gradient(top_class, images)return grads
def get_integrated_gradients(img_input, top_pred_idx, baseline=None, num_steps=50):"""Computes Integrated Gradients for a predicted label.
Args:img_input (ndarray): Original imagetop_pred_idx: Predicted label for the input imagebaseline (ndarray): The baseline image to start with for interpolationnum_steps: Number of interpolation steps between the baselineand the input used in the computation of integrated gradients. Thesesteps along determine the integral approximation error. By default,num_steps is set to 50.
Returns:Integrated gradients w.r.t input image"""    # If baseline is not provided, start with a black image    # having same size as the input image.if baseline is None:baseline = np.zeros(img_size).astype(np.float32)else:baseline = baseline.astype(np.float32)
    # 1. Do interpolation.img_input = img_input.astype(np.float32)interpolated_image = [baseline + (step / num_steps) * (img_input - baseline)for step in range(num_steps + 1)]interpolated_image = np.array(interpolated_image).astype(np.float32)
    # 2. Preprocess the interpolated imagesinterpolated_image = xception.preprocess_input(interpolated_image)
    # 3. Get the gradientsgrads = []for i, img in enumerate(interpolated_image):img = tf.expand_dims(img, axis=0)grad = get_gradients(img, top_pred_idx=top_pred_idx)grads.append(grad[0])grads = tf.convert_to_tensor(grads, dtype=tf.float32)
    # 4. Approximate the integral usiing the trapezoidal rulegrads = (grads[:-1] + grads[1:]) / 2.0avg_grads = tf.reduce_mean(grads, axis=0)
    # 5. Calculate integrated gradients and returnintegrated_grads = (img_input - baseline) * avg_gradsreturn integrated_grads
def random_baseline_integrated_gradients(img_input, top_pred_idx, num_steps=50, num_runs=2):"""Generates a number of random baseline images.
Args:img_input (ndarray): 3D imagetop_pred_idx: Predicted label for the input imagenum_steps: Number of interpolation steps between the baselineand the input used in the computation of integrated gradients. Thesesteps along determine the integral approximation error. By default,num_steps is set to 50.num_runs: number of baseline images to generate
Returns:Averaged integrated gradients for `num_runs` baseline images"""    # 1. List to keep track of Integrated Gradients (IG) for all the imagesintegrated_grads = []
    # 2. Get the integrated gradients for all the baselinesfor run in range(num_runs):baseline = np.random.random(img_size) * 255igrads = get_integrated_gradients(img_input=img_input,top_pred_idx=top_pred_idx,baseline=baseline,num_steps=num_steps,)integrated_grads.append(igrads)
    # 3. Return the average integrated gradients for the imageintegrated_grads = tf.convert_to_tensor(integrated_grads)return tf.reduce_mean(integrated_grads, axis=0)

可視化梯度與集成梯度

class GradVisualizer:"""Plot gradients of the outputs w.r.t an input image."""
def __init__(self, positive_channel=None, negative_channel=None):if positive_channel is None:self.positive_channel = [0, 255, 0]else:self.positive_channel = positive_channel
if negative_channel is None:self.negative_channel = [255, 0, 0]else:self.negative_channel = negative_channel
def apply_polarity(self, attributions, polarity):if polarity == "positive":return np.clip(attributions, 0, 1)else:return np.clip(attributions, -1, 0)
def apply_linear_transformation(self,attributions,clip_above_percentile=99.9,clip_below_percentile=70.0,lower_end=0.2,):        # 1. Get the thresholdsm = self.get_thresholded_attributions(attributions, percentage=100 - clip_above_percentile)e = self.get_thresholded_attributions(attributions, percentage=100 - clip_below_percentile)
        # 2. Transform the attributions by a linear function f(x) = a*x + b such that        # f(m) = 1.0 and f(e) = lower_endtransformed_attributions = (1 - lower_end) * (np.abs(attributions) - e) / (m - e) + lower_end
        # 3. Make sure that the sign of transformed attributions is the same as original attributionstransformed_attributions *= np.sign(attributions)
        # 4. Only keep values that are bigger than the lower_endtransformed_attributions *= transformed_attributions >= lower_end
        # 5. Clip values and returntransformed_attributions = np.clip(transformed_attributions, 0.0, 1.0)return transformed_attributions
def get_thresholded_attributions(self, attributions, percentage):if percentage == 100.0:return np.min(attributions)
        # 1. Flatten the attributionsflatten_attr = attributions.flatten()
        # 2. Get the sum of the attributionstotal = np.sum(flatten_attr)
        # 3. Sort the attributions from largest to smallest.sorted_attributions = np.sort(np.abs(flatten_attr))[::-1]
        # 4. Calculate the percentage of the total sum that each attribution        # and the values about it contribute.cum_sum = 100.0 * np.cumsum(sorted_attributions) / total
        # 5. Threshold the attributions by the percentageindices_to_consider = np.where(cum_sum >= percentage)[0][0]
        # 6. Select the desired attributions and returnattributions = sorted_attributions[indices_to_consider]return attributions
def binarize(self, attributions, threshold=0.001):return attributions > threshold
def morphological_cleanup_fn(self, attributions, structure=np.ones((4, 4))):closed = ndimage.grey_closing(attributions, structure=structure)opened = ndimage.grey_opening(closed, structure=structure)return opened
def draw_outlines(self, attributions, percentage=90, connected_component_structure=np.ones((3, 3))):        # 1. Binarize the attributions.attributions = self.binarize(attributions)
        # 2. Fill the gapsattributions = ndimage.binary_fill_holes(attributions)
        # 3. Compute connected componentsconnected_components, num_comp = ndimage.measurements.label(attributions, structure=connected_component_structure)
        # 4. Sum up the attributions for each componenttotal = np.sum(attributions[connected_components > 0])component_sums = []for comp in range(1, num_comp + 1):mask = connected_components == compcomponent_sum = np.sum(attributions[mask])component_sums.append((component_sum, mask))
        # 5. Compute the percentage of top components to keepsorted_sums_and_masks = sorted(component_sums, key=lambda x: x[0], reverse=True)sorted_sums = list(zip(*sorted_sums_and_masks))[0]cumulative_sorted_sums = np.cumsum(sorted_sums)cutoff_threshold = percentage * total / 100cutoff_idx = np.where(cumulative_sorted_sums >= cutoff_threshold)[0][0]if cutoff_idx > 2:cutoff_idx = 2
        # 6. Set the values for the kept componentsborder_mask = np.zeros_like(attributions)for i in range(cutoff_idx + 1):border_mask[sorted_sums_and_masks[i][1]] = 1
        # 7. Make the mask hollow and show only the bordereroded_mask = ndimage.binary_erosion(border_mask, iterations=1)border_mask[eroded_mask] = 0
        # 8. Return the outlined maskreturn border_mask
def process_grads(self,image,attributions,polarity="positive",clip_above_percentile=99.9,clip_below_percentile=0,morphological_cleanup=False,structure=np.ones((3, 3)),outlines=False,outlines_component_percentage=90,overlay=True,):if polarity not in ["positive", "negative"]:raise ValueError(f""" Allowed polarity values: 'positive' or 'negative'but provided {polarity}""")if clip_above_percentile  100:raise ValueError("clip_above_percentile must be in [0, 100]")
if clip_below_percentile  100:raise ValueError("clip_below_percentile must be in [0, 100]")
        # 1. Apply polarityif polarity == "positive":attributions = self.apply_polarity(attributions, polarity=polarity)channel = self.positive_channelelse:attributions = self.apply_polarity(attributions, polarity=polarity)attributions = np.abs(attributions)channel = self.negative_channel
        # 2. Take average over the channelsattributions = np.average(attributions, axis=2)
        # 3. Apply linear transformation to the attributionsattributions = self.apply_linear_transformation(attributions,clip_above_percentile=clip_above_percentile,clip_below_percentile=clip_below_percentile,lower_end=0.0,)
        # 4. Cleanupif morphological_cleanup:attributions = self.morphological_cleanup_fn(attributions, structure=structure)        # 5. Draw the outlinesif outlines:attributions = self.draw_outlines(attributions, percentage=outlines_component_percentage)
        # 6. Expand the channel axis and convert to RGBattributions = np.expand_dims(attributions, 2) * channel
        # 7.Superimpose on the original imageif overlay:attributions = np.clip((attributions * 0.8 + image), 0, 255)return attributions
def visualize(self,image,gradients,integrated_gradients,polarity="positive",clip_above_percentile=99.9,clip_below_percentile=0,morphological_cleanup=False,structure=np.ones((3, 3)),outlines=False,outlines_component_percentage=90,overlay=True,figsize=(15, 8),):        # 1. Make two copies of the original imageimg1 = np.copy(image)img2 = np.copy(image)
        # 2. Process the normal gradientsgrads_attr = self.process_grads(image=img1,attributions=gradients,polarity=polarity,clip_above_percentile=clip_above_percentile,clip_below_percentile=clip_below_percentile,morphological_cleanup=morphological_cleanup,structure=structure,outlines=outlines,outlines_component_percentage=outlines_component_percentage,overlay=overlay,)
        # 3. Process the integrated gradientsigrads_attr = self.process_grads(image=img2,attributions=integrated_gradients,polarity=polarity,clip_above_percentile=clip_above_percentile,clip_below_percentile=clip_below_percentile,morphological_cleanup=morphological_cleanup,structure=structure,outlines=outlines,outlines_component_percentage=outlines_component_percentage,overlay=overlay,)
_, ax = plt.subplots(1, 3, figsize=figsize)ax[0].imshow(image)ax[1].imshow(grads_attr.astype(np.uint8))ax[2].imshow(igrads_attr.astype(np.uint8))
ax[0].set_title("Input")ax[1].set_title("Normal gradients")ax[2].set_title("Integrated gradients")plt.show()

預測

# 1. Convert the image to numpy arrayimg = get_img_array(img_path)
# 2. Keep a copy of the original imageorig_img = np.copy(img[0]).astype(np.uint8)
# 3. Preprocess the imageimg_processed = tf.cast(xception.preprocess_input(img), dtype=tf.float32)
# 4. Get model predictionspreds = model.predict(img_processed)top_pred_idx = tf.argmax(preds[0])print("Predicted:", top_pred_idx, xception.decode_predictions(preds, top=1)[0])
# 5. Get the gradients of the last layer for the predicted labelgrads = get_gradients(img_processed, top_pred_idx=top_pred_idx)
# 6. Get the integrated gradientsigrads = random_baseline_integrated_gradients(np.copy(orig_img), top_pred_idx=top_pred_idx, num_steps=50, num_runs=2)
# 7. Process the gradients and plotvis = GradVisualizer()vis.visualize(image=orig_img,gradients=grads[0].numpy(),integrated_gradients=igrads.numpy(),clip_above_percentile=99,clip_below_percentile=0,)
vis.visualize(image=orig_img,gradients=grads[0].numpy(),integrated_gradients=igrads.numpy(),clip_above_percentile=95,clip_below_percentile=28,morphological_cleanup=True,outlines=True,)

公眾號:專知

相關文章

證明 π 是無理數的方法,高中生也能理解

證明 π 是無理數的方法,高中生也能理解

▌前言 我們都知道圓周率 是無理數,但極少有人知道怎麼證明它。事實上,很多專業的數學學者也不了解具體的證明方法。究其原因,一是沒必要、二是大...

你真的會解方程嗎?

你真的會解方程嗎?

認真閱讀下面的文章,並思考文末互動提出的問題,嚴格按照 互動:你的答案 格式在評論區留言,就有機會獲得由江蘇鳳凰科學技術出版社提供的優質科普...

坐上時光機,帶你穿越到40億年前

坐上時光機,帶你穿越到40億年前

地球已經度過近46億年的漫長歲月,從最初太陽系的形成到現在繁榮的人類文明,在這期間地球上曾發生過太多值得我們探索的秘密。讓我們以地球紀元為線...

物理學家心目中的物理學家

物理學家心目中的物理學家

說起物理學,你會想到哪些名字? 用保羅·戴維斯的話說,「一些科學家很偉大,是因為他們是出色的全才。一些人可能是偶然做出了一個重大發現,但有運...

關於量子力學的基本原理

關於量子力學的基本原理

|作者:鄭偉謀† (中國科學院理論物理研究所) 本文發表於《物理》2020年第10期 ■推薦理由 關於量子力學基本原理的介紹和討論,有助於澄...

17個機器學習的常用演算法!

17個機器學習的常用演算法!

根據資料類型的不同,對一個問題的建模有不同的方式。在機器學習或者人工智慧領域,人們首先會考慮演算法的學習方式。在機器學習領域,有幾種主要的學...

150年的解密之旅

150年的解密之旅

無論是我們追問「我們是誰」,還是探尋「我們從何而來」,甚至是暢想「我們即將前往何方」的時候,最終都離不開的一個詞——遺傳。而在遺傳的背後,那...

你也能懂的馬克士威方程組

你也能懂的馬克士威方程組

這篇文章我們來學習馬克士威方程組的微分形式。 在積分篇裡,我們一直在跟電場、磁場的通量打交道。我們任意畫一個曲面,這個曲面可以是閉合的,也可...