Algorithm preparation¶
I've decided to use three images describing peking duck, paella and panna cotta. I divided each of them in 49 regions like on image:
+
Generating subimages and calculating predictions for them¶
I generated subimages by drawing vector of size number of regions from uniform distribution and graying out certain super-pixel if it is above 0.5 threshold:
+
The image with grayed-out super pixels is z from the paper and boolean vector (mapped later to float) indicating if super-pixel is chosen is z' from the LIME paper. I calculated predictions f(z) just by using my clasifier on each subimage.
Calculating LIME explanaitions¶
Explanation g is a function from the z' space to reals described by dot product of z' and some weights w so to calculate explanaitions I need to calculate theese weights. For this I must calculate proximity measures pi(z) so I calculate them using the equation from the LIME paper and I have whole set Z from the paper by now.
I fixed constance K to 10 and made K-Lasso writen in the paper which is selecting K features (z''s are features here) using Lasso with regularization and then learned the weights for non-zero features z' via least squares having f(z)'s as targets - I used scikit-learn functions for this step.
Then explanaitions g can be implemented, becouse they have this weights internally.
Remark¶
Most variables in scripts in appendix are dicts with keys being image names. Just provide image name in [] to get access to variable for image of interest.
Explanations visualization¶
I have visualized obtained explanations after rescailing their values (red are for negative values, green for positive).
+My conclusion is that model may be thinking that food packaging is also important becouse explanations show mostly food packagings or other items related to food and not so often they show food.
+Explanation for peking duck¶

Explanation for paella¶

Explanation for panna cotta¶

Appendix¶
+Used library imports¶
+from sklearn.linear_model import Lasso
+from sklearn.linear_model import LinearRegression
+from transformers import pipeline
+import matplotlib.pyplot as plt
+from PIL import Image
+import numpy as np
+from copy import deepcopy
+Initial data preparation and constances initialization¶
+class ImageName:
+ PEKING_DUCK = 'peking_duck'
+ PAELLA = 'paella'
+ PANNA_COTTA = 'panna_cotta'
+
+IMAGE_NAMES = [ImageName.PEKING_DUCK, ImageName.PAELLA, ImageName.PANNA_COTTA]
+SELECTED_OBSERVATIONS = {image_name: Image.open(f'{image_name}.jpg') for image_name in IMAGE_NAMES}
+CHOSEN_VISION_MODEL = "Shresthadev403/food-image-classification"
+TASK = "image-classification"
+DEVICE = "cuda"
+SELECTED_NUMBER_OF_REGIONS = 49
+N = 100
+K = 10
+
+
+for observation in SELECTED_OBSERVATIONS.values():
+ plt.axis('off'); plt.imshow(observation); plt.show()
+Calculate the predictions for selected observations¶
+pipe = pipeline(TASK, device=DEVICE, model=CHOSEN_VISION_MODEL)
+def calculate_the_predictions_for_selected_observations(pipe, observations):
+ pipe = pipeline(TASK, device=DEVICE, model=CHOSEN_VISION_MODEL)
+ best_predictions = {
+ image_name: predictions[0]
+ for image_name, predictions
+ in zip(SELECTED_OBSERVATIONS.keys(), pipe(list(SELECTED_OBSERVATIONS.values())))
+ }
+ return best_predictions
+
+predictions_for_observations = calculate_the_predictions_for_selected_observations(pipe, SELECTED_OBSERVATIONS)
+predictions_for_observations
+Divide images in grids of rectangles¶
+class GridOfRectangles:
+ """Class for calculating regions"""
+ def __init__(self, image, number_of_regions: int):
+ self.number_of_regions = number_of_regions
+ self.h, self.w = self.get_sides()
+ self.array = self.calculate_regions(image)
+
+ def get_sides(self):
+ """Chooses grid dimentions for non-prime number of regions greater than 3"""
+ if self.number_of_regions <= 3:
+ raise ValueError(f"Provide number greater than 3")
+ for i in range(int(self.number_of_regions ** 0.5 + 1), 1, -1):
+ if self.number_of_regions % i == 0:
+ return i, self.number_of_regions // i
+ raise ValueError(f"{self.number_of_regions} is prime, provide not prime number")
+
+ def calculate_regions(self, image):
+ array = []
+ parts = np.array_split(image, self.h)
+ for part in parts:
+ array.append(np.array_split(part, self.w, axis=1))
+ return array
+
+ def visualize(self):
+ f, axs = plt.subplots(self.h, self.w)
+ for i, row in enumerate(self.array):
+ for j, element in enumerate(row):
+ axs[i, j].imshow(element)
+ axs[i, j].set_xticklabels([])
+ axs[i, j].set_yticklabels([])
+ axs[i, j].axis('off')
+
+ plt.subplots_adjust(wspace=0.1, hspace=0.1)
+ plt.show()
+
+observations = {image_name: np.array(observation) for image_name, observation in SELECTED_OBSERVATIONS.items()}
+regions = {image_name: GridOfRectangles(image, SELECTED_NUMBER_OF_REGIONS) for image_name, image in observations.items()}
+
+for region in regions.values():
+ region.visualize()
+Generate subimages and calculate predictions for them¶
+def generate_sample_in_interpretable_representation():
+ return np.random.random_sample(size=SELECTED_NUMBER_OF_REGIONS) < 0.5
+
+def generate_gray_rectangle(h, w):
+ return np.full((h, w, 3), 128, dtype=np.uint8)
+
+def calculate_subimage_score(subimage_prediction, label):
+ for pred in subimage_prediction:
+ if pred['label'] == label:
+ return pred['score']
+ return 0.
+
+z_prims = {
+ ImageName.PEKING_DUCK: [generate_sample_in_interpretable_representation() for _ in range(N)],
+ ImageName.PAELLA: [generate_sample_in_interpretable_representation() for _ in range(N)],
+ ImageName.PANNA_COTTA: [generate_sample_in_interpretable_representation() for _ in range(N)]
+}
+
+zs = {
+ image_name: [
+ np.concatenate([
+ np.concatenate([
+ element if selected[i * grid.w + j]
+ else generate_gray_rectangle(element.shape[0], element.shape[1])
+ for j, element in enumerate(row)
+ ], axis=1) for i, row in enumerate(grid.array)
+ ])
+ for selected in z_prims[image_name]
+ ] for image_name, grid in regions.items()
+}
+
+z_prims = {k: np.array(z_prim).astype(float) for k, z_prim in z_prims.items()}
+
+f_zs = {
+ k: np.array([
+ calculate_subimage_score(pipe(Image.fromarray(z)), predictions_for_observations[k]['label'])
+ for z in zs[k]
+ ])
+ for k in zs.keys()
+}
+
+plt.axis('off')
+plt.imshow(zs[ImageName.PEKING_DUCK][0])
+plt.show()
+Calculate LIME explanations¶
+def get_pi(x):
+ def D(x, z):
+ return np.sum((x.astype(np.int64) - z.astype(np.int64)) ** 2) ** 0.5
+ sigma = x.shape[1]
+ return lambda z: np.exp(-D(x, z) / (sigma ** 2))
+
+def k_lasso(z_prims, f_z, k, alpha=0.1):
+ lasso_model = Lasso(alpha=alpha)
+ lasso_model.fit(z_prims, f_z)
+ coefs = np.abs(lasso_model.coef_)
+ top_k_features = np.argsort(coefs)[-k:]
+ res = np.zeros(shape=(z_prims.shape[1],), dtype=bool)
+ res[top_k_features] = True
+ return res
+
+pi_x = {k: get_pi(x) for k, x in observations.items()}
+pi_zs = {k: [pi_x[k](z) for z in zs[k]] for k in zs.keys()}
+selected_features_mask = {image_name: k_lasso(z_prim, f_zs[image_name], K) for image_name, z_prim in z_prims.items()}
+selected_features = {image_name: z_prims[image_name][:, mask] for image_name, mask in selected_features_mask.items()}
+
+w = {image_name: np.zeros(shape=(SELECTED_NUMBER_OF_REGIONS,), dtype=float) for image_name in selected_features.keys()}
+
+for image_name, features in selected_features.items():
+ model = LinearRegression()
+ model.fit(features, f_zs[image_name])
+ w[image_name][selected_features_mask[image_name]] = model.coef_
+
+g = {
+ image_name: (lambda z_prim: np.sum(w_g * z_prim)) for image_name, w_g in w.items()
+}
+Visualize explanaitions¶
+spaced_weights = np.array([weight.reshape(region.h, region.w) for weight, region in zip(w.values(), regions.values())])
+positive_weights = spaced_weights.copy(); positive_weights[positive_weights < 1e-16] = 0
+negative_weights = -spaced_weights.copy(); negative_weights[negative_weights < 1e-16] = 0
+positive_rescaled = (positive_weights - positive_weights.min(axis=(1, 2))[:, None, None]) / (positive_weights.max(axis=(1, 2))[:, None, None] - positive_weights.min(axis=(1, 2))[:, None, None])
+negative_rescaled = (negative_weights - negative_weights.min(axis=(1, 2))[:, None, None]) / (negative_weights.max(axis=(1, 2))[:, None, None] - negative_weights.min(axis=(1, 2))[:, None, None])
+
+def apply_influence(element, idx, i, j):
+ positive = np.full_like(element, positive_rescaled[idx, i, j], dtype=float)
+ negative = np.full_like(element, negative_rescaled[idx, i, j], dtype=float)
+ positive[:, :, [0, 2]] = 0; negative[:, :, [1, 2]] = 0
+ influence = positive + negative
+ return element * (1 - positive_rescaled[idx, i, j] - negative_rescaled[idx, i, j]) + element * influence
+
+influences = [
+ np.concatenate([
+ np.concatenate([
+ apply_influence(element, idx, i, j)
+ for j, element in enumerate(row)
+ ], axis=1) for i, row in enumerate(grid.array)
+ ]).astype(np.uint8)
+ for idx, (image_name, grid) in enumerate(regions.items())
+]
+
+for influence in influences:
+ plt.axis('off')
+ plt.imshow(influence)
+ plt.show()
+