PROJECT_ID = "[your-project-id]"

if PROJECT_ID == "" or PROJECT_ID is None or PROJECT_ID == "[your-project-id]":
    # Get your GCP project id from gcloud
    shell_output = !gcloud config list --format 'value(core.project)' 2>/dev/null
    PROJECT_ID = shell_output[0]
    print("Project ID:", PROJECT_ID)

!gcloud config set project $PROJECT_ID

"""
Explanation: AI Explanations: Deploying an image model
Overview
This tutorial shows how to train a Keras classification model on image data and deploy it to the AI Platform Explanations service to get feature attributions on your deployed model.
If you've already got a trained model and want to deploy it to AI Explanations, skip to the Export the model as a TF 2 SavedModel section.
Dataset
The dataset used for this tutorial is the flowers dataset from TensorFlow Datasets.
Objective
The goal of this tutorial is to train a model on a simple image dataset (flower classification) to understand how you can use AI Explanations with image models. For image models, AI Explanations returns an image with the pixels highlighted that signaled your model's prediction the most.
This tutorial focuses more on deploying the model to AI Platform with Explanations than on the design of the model itself. 
Setup
End of explanation
"""


REGION = 'us-central1'

"""
Explanation: Region
You can also change the REGION variable, which is used for operations
throughout the rest of this notebook. Make sure to choose a region where Cloud
AI Platform services are
available. You can
not use a Multi-Regional Storage bucket for training with AI Platform.
End of explanation
"""


from datetime import datetime

TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")

"""
Explanation: Timestamp
If you are in a live tutorial session, you might be using a shared test account or project. To avoid name collisions between users on resources created, you create a timestamp for each instance session, and append onto the name of resources which will be created in this tutorial.
End of explanation
"""


BUCKET_NAME = "[your-bucket-name]"

if BUCKET_NAME == "" or BUCKET_NAME is None or BUCKET_NAME == "[your-bucket-name]":
    BUCKET_NAME = PROJECT_ID + "_xai_flowers_" + TIMESTAMP

"""
Explanation: Create a Cloud Storage bucket
When you submit a training job using the Cloud SDK, you upload a Python package
containing your training code to a Cloud Storage bucket. AI Platform runs
the code from this package. In this tutorial, AI Platform also saves the
trained model that results from your job in the same bucket. You can then
create an AI Platform model version based on this output in order to serve
online predictions.
Set the name of your Cloud Storage bucket below. It must be unique across all
Cloud Storage buckets.
End of explanation
"""


! gsutil mb -l $REGION gs://$BUCKET_NAME

"""
Explanation: Only if your bucket doesn't already exist: Run the following cell to create your Cloud Storage bucket.
End of explanation
"""


# Install explainable-ai-sdk
!pip install explainable-ai-sdk

import io
import os
import random
import numpy as np
import PIL
import tensorflow as tf

from matplotlib import pyplot as plt
from base64 import b64encode

# should be >= 2.1
print("Tensorflow version " + tf.__version__)
if tf.__version__ < "2.1":
    raise Exception("TF 2.1 or greater is required")

AUTO = tf.data.experimental.AUTOTUNE
print("AUTO", AUTO)

import explainable_ai_sdk

"""
Explanation: Import libraries
Import the libraries for this tutorial. This tutorial has been tested with TensorFlow versions 2.3.
End of explanation
"""


GCS_PATTERN = 'gs://flowers-public/tfrecords-jpeg-192x192-2/*.tfrec'
IMAGE_SIZE = [192, 192]

BATCH_SIZE = 32

VALIDATION_SPLIT = 0.19
CLASSES = ['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']  # do not change, maps to the labels in the data (folder names)

# Split data files between training and validation
filenames = tf.io.gfile.glob(GCS_PATTERN)
random.shuffle(filenames)
split = int(len(filenames) * VALIDATION_SPLIT)
training_filenames = filenames[split:]
validation_filenames = filenames[:split]
print("Pattern matches {} data files. Splitting dataset into {} training files and {} validation files".format(len(filenames), len(training_filenames), len(validation_filenames)))
validation_steps = int(3670 // len(filenames) * len(validation_filenames)) // BATCH_SIZE
steps_per_epoch = int(3670 // len(filenames) * len(training_filenames)) // BATCH_SIZE
print("With a batch size of {}, there will be {} batches per training epoch and {} batch(es) per validation run.".format(BATCH_SIZE, steps_per_epoch, validation_steps))

"""
Explanation: Download and preprocess the data
This section shows how to download the flower images, use the tf.data API to create a data input pipeline, and split the data into training and validation sets.
End of explanation
"""


# @title display utilities [RUN ME]

def dataset_to_numpy_util(dataset, N):
    dataset = dataset.batch(N)

    if tf.executing_eagerly():
        #  In eager mode, iterate in the Dataset directly.
        for images, labels in dataset:
            numpy_images = images.numpy()
            numpy_labels = labels.numpy()
            break
    else:
        # In non-eager mode, must get the TF note that
        # yields the nextitem and run it in a tf.Session.
        get_next_item = dataset.make_one_shot_iterator().get_next()
        with tf.Session() as ses:
            numpy_images, numpy_labels = ses.run(get_next_item)

    return numpy_images, numpy_labels


def title_from_label_and_target(label, correct_label):
    label = np.argmax(label, axis=-1)  # one-hot to class number
    correct_label = np.argmax(correct_label, axis=-1)  # one-hot to class number
    correct = (label == correct_label)
    return "{} [{}{}{}]".format(CLASSES[label], str(correct), ', shoud be ' if not correct else '',
                                CLASSES[correct_label] if not correct else ''), correct


def display_one_flower(image, title, subplot, red=False):
    plt.subplot(subplot)
    plt.axis('off')
    plt.imshow(image)
    plt.title(title, fontsize=16, color='red' if red else 'black')
    return subplot + 1


def display_9_images_from_dataset(dataset):
    subplot = 331
    plt.figure(figsize=(13, 13))
    images, labels = dataset_to_numpy_util(dataset, 9)
    for i, image in enumerate(images):
        title = CLASSES[np.argmax(labels[i], axis=-1)]
        subplot = display_one_flower(image, title, subplot)
        if i >= 8:
            break

    plt.tight_layout()
    plt.subplots_adjust(wspace=0.1, hspace=0.1)
    plt.show()


def display_9_images_with_predictions(images, predictions, labels):
    subplot = 331
    plt.figure(figsize=(13, 13))
    for i, image in enumerate(images):
        title, correct = title_from_label_and_target(predictions[i], labels[i])
        subplot = display_one_flower(image, title, subplot, not correct)
        if i >= 8:
            break

    plt.tight_layout()
    plt.subplots_adjust(wspace=0.1, hspace=0.1)
    plt.show()


def display_training_curves(training, validation, title, subplot):
    if subplot % 10 == 1:  # set up the subplots on the first call
        plt.subplots(figsize=(10, 10), facecolor='#F0F0F0')
        plt.tight_layout()
    ax = plt.subplot(subplot)
    ax.set_facecolor('#F8F8F8')
    ax.plot(training)
    ax.plot(validation)
    ax.set_title('model ' + title)
    ax.set_ylabel(title)
    ax.set_xlabel('epoch')
    ax.legend(['train', 'valid.'])

"""
Explanation: The following cell contains some image visualization utility functions. This code isn't essential to training or deploying the model. 
If you're running this from Colab the cell is hidden. You can look at the code by right clicking on the cell --> "Form" --> "Show form" if you'd like to see it.
End of explanation
"""


def read_tfrecord(example):
    features = {
        "image": tf.io.FixedLenFeature([], tf.string),  # tf.string means bytestring
        "class": tf.io.FixedLenFeature([], tf.int64),  # shape [] means scalar
        "one_hot_class": tf.io.VarLenFeature(tf.float32),
    }
    example = tf.io.parse_single_example(example, features)
    image = tf.image.decode_jpeg(example['image'], channels=3)
    image = tf.cast(image, tf.float32) / 255.0  # convert image to floats in [0, 1] range
    image = tf.reshape(image, [*IMAGE_SIZE, 3])  # explicit size will be needed for TPU
    one_hot_class = tf.sparse.to_dense(example['one_hot_class'])
    one_hot_class = tf.reshape(one_hot_class, [5])
    return image, one_hot_class


def load_dataset(filenames):
    # Read data from TFRecords

    dataset = tf.data.Dataset.from_tensor_slices(filenames)
    dataset = dataset.interleave(tf.data.TFRecordDataset, cycle_length=16, num_parallel_calls=AUTO)  # faster
    dataset = dataset.map(read_tfrecord, num_parallel_calls=AUTO)
    return dataset

"""
Explanation: Read images and labels from TFRecords
In this dataset the images are stored as TFRecords.
End of explanation
"""


display_9_images_from_dataset(load_dataset(training_filenames))

"""
Explanation: Use the visualization utility function provided earlier to preview flower images with their labels.
End of explanation
"""


def get_batched_dataset(filenames):
    dataset = load_dataset(filenames)
    dataset = dataset.cache()  # This dataset fits in RAM
    dataset = dataset.repeat()
    dataset = dataset.shuffle(2048)
    dataset = dataset.batch(BATCH_SIZE)
    dataset = dataset.prefetch(AUTO)  # prefetch next batch while training (autotune prefetch buffer size)
    # For proper ordering of map/batch/repeat/prefetch, see Dataset performance guide: https://www.tensorflow.org/guide/performance/datasets
    return dataset


def get_training_dataset():
    return get_batched_dataset(training_filenames)


def get_validation_dataset():
    return get_batched_dataset(validation_filenames)


some_flowers, some_labels = dataset_to_numpy_util(load_dataset(validation_filenames), 8 * 20)

"""
Explanation: Create training and validation datasets
End of explanation
"""


from tensorflow.keras import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Dense, GlobalAveragePooling2D, BatchNormalization
from tensorflow.keras.optimizers import Adam

model = Sequential([
    # Stem
    Conv2D(kernel_size=3, filters=16, padding='same', activation='relu', input_shape=[*IMAGE_SIZE, 3]),
    BatchNormalization(),
    Conv2D(kernel_size=3, filters=32, padding='same', activation='relu'),
    BatchNormalization(),
    MaxPooling2D(pool_size=2),

    # Conv Group
    Conv2D(kernel_size=3, filters=64, padding='same', activation='relu'),
    BatchNormalization(),
    MaxPooling2D(pool_size=2),
    Conv2D(kernel_size=3, filters=96, padding='same', activation='relu'),
    BatchNormalization(),
    MaxPooling2D(pool_size=2),

    # Conv Group
    Conv2D(kernel_size=3, filters=128, padding='same', activation='relu'),
    BatchNormalization(),
    MaxPooling2D(pool_size=2),
    Conv2D(kernel_size=3, filters=128, padding='same', activation='relu'),
    BatchNormalization(),

    # 1x1 Reduction
    Conv2D(kernel_size=1, filters=32, padding='same', activation='relu'),
    BatchNormalization(),

    # Classifier
    GlobalAveragePooling2D(),
    Dense(5, activation='softmax')
])

model.compile(
    optimizer=Adam(lr=0.005, decay=0.98),
    loss='categorical_crossentropy',
    metrics=['accuracy'])
model.summary()

"""
Explanation: Build, train, and evaluate the model
This section shows how to build, train, evaluate, and get local predictions from a model by using the TF.Keras Sequential API.
End of explanation
"""


EPOCHS = 20  # Train for 60 epochs for higher accuracy, 20 should get you ~75%

history = model.fit(get_training_dataset(), steps_per_epoch=steps_per_epoch, epochs=EPOCHS,
                    validation_data=get_validation_dataset(), validation_steps=validation_steps)

"""
Explanation: Train the model
Train this on a GPU if you have access (in Colab, from the menu select Runtime --> Change runtime type). On a CPU, it'll take ~30 minutes to run training. On a GPU, it takes ~5 minutes.
End of explanation
"""


# Randomize the input so that you can execute multiple times to change results
permutation = np.random.permutation(8 * 20)
some_flowers, some_labels = (some_flowers[permutation], some_labels[permutation])

predictions = model.predict(some_flowers, batch_size=16)
evaluations = model.evaluate(some_flowers, some_labels, batch_size=16)

print(np.array(CLASSES)[np.argmax(predictions, axis=-1)].tolist())
print('[val_loss, val_acc]', evaluations)

display_9_images_with_predictions(some_flowers, predictions, some_labels)

"""
Explanation: Visualize local predictions
Get predictions on your local model and visualize the images with their predicted labels, using the visualization utility function provided earlier.
End of explanation
"""


export_path = 'gs://' + BUCKET_NAME + '/explanations/mymodel'


def _preprocess(bytes_input):
    decoded = tf.io.decode_jpeg(bytes_input, channels=3)
    decoded = tf.image.convert_image_dtype(decoded, tf.float32)
    resized = tf.image.resize(decoded, size=(192, 192))
    return resized


@tf.function(input_signature=[tf.TensorSpec([None], tf.string)])
def preprocess_fn(bytes_inputs):
    with tf.device("cpu:0"):
        decoded_images = tf.map_fn(_preprocess, bytes_inputs, dtype=tf.float32)
    return {"numpy_inputs": decoded_images}  # User needs to make sure the key matches model's input


m_call = tf.function(model.call).get_concrete_function([tf.TensorSpec(shape=[None, 192, 192, 3], dtype=tf.float32, name="numpy_inputs")])


@tf.function(input_signature=[tf.TensorSpec([None], tf.string)])
def serving_fn(bytes_inputs):
    images = preprocess_fn(bytes_inputs)
    prob = m_call(**images)
    return prob


tf.saved_model.save(model, export_path, signatures={
    'serving_default': serving_fn,
    'xai_preprocess': preprocess_fn,  # Required for XAI
    'xai_model': m_call  # Required for XAI
})

"""
Explanation: Export the model as a TF 2.x SavedModel
When using TensorFlow 2.x, you export the model as a SavedModel and load it into Cloud Storage. During export, you need to define a serving function to convert data to the format your model expects. If you send encoded data to AI Platform, your serving function ensures that the data is decoded on the model server before it is passed as input to your model.
Serving function for image data
Sending base 64 encoded image data to AI Platform is more space efficient. Since this deployed model expects input data as raw bytes, you need to ensure that the b64 encoded data gets converted back to raw bytes before it is passed as input to the deployed model.
To resolve this, define a serving function (serving_fn) and attach it to the model as a preprocessing step. Add a @tf.function decorator so the serving function is part of the model's graph (instead of upstream on a CPU).
When you send a prediction or explanation request, the request goes to the serving function (serving_fn), which preprocesses the b64 bytes into raw numpy bytes (preprocess_fn). At this point, the data can be passed to the model (m_call).
End of explanation
"""


! saved_model_cli show --dir $export_path --all

"""
Explanation: Get input and output signatures
Use TensorFlow's saved_model_cli to inspect the model's SignatureDef. You'll use this information when you deploy your model to AI Explanations in the next section.
End of explanation
"""


loaded = tf.saved_model.load(export_path)

input_name = list(loaded.signatures['xai_model'].structured_input_signature[1].keys())[0]
print(input_name)

output_name = list(loaded.signatures['xai_model'].structured_outputs.keys())[0]
print(output_name)

preprocess_name = list(loaded.signatures['xai_preprocess'].structured_input_signature[1].keys())[0]
print(preprocess_name)

"""
Explanation: You can get the signatures of your model's input and output layers by reloading the model into memory, and querying it for the signatures corresponding to each layer.
You need the signatures for the following layers:

Serving function input layer
Model input layer
Model output layer
End of explanation
"""


from explainable_ai_sdk.metadata.tf.v2 import SavedModelMetadataBuilder
# We want to explain 'xai_model' signature.
builder = SavedModelMetadataBuilder(export_path, signature_name='xai_model')
random_baseline = np.random.rand(192, 192, 3)
builder.set_image_metadata(
    'numpy_inputs',
    input_baselines=[random_baseline.tolist()])
builder.save_metadata(export_path)

"""
Explanation: Generate explanation metadata
In order to deploy this model to AI Explanations, you need to create an explanation_metadata.json file with information about your model inputs, outputs, and baseline. You can use the Explainable AI SDK to generate most of the fields.
For image models, using [0,1] as your input baseline represents black and white images. This example uses np.random to generate the baseline because the training images contain a lot of black and white (i.e. daisy petals).
Note: for the explanation request, use the model's signature for the input and output tensors. Do not use the serving function signature.
End of explanation
"""


import datetime
MODEL = 'flowers' + TIMESTAMP
print(MODEL)

# Create the model if it doesn't exist yet (you only need to run this once)
! gcloud ai-platform models create $MODEL --enable-logging --regions=$REGION

"""
Explanation: Deploy model to AI Explanations
This section shows how to use gcloud to deploy the model to AI Explanations, using two different explanation methods for image models.
Create the model
End of explanation
"""


# Each time you create a version the name should be unique
IG_VERSION = 'v_ig'

! gcloud beta ai-platform versions create $IG_VERSION \
--model $MODEL \
--origin $export_path \
--runtime-version 2.3 \
--framework TENSORFLOW \
--python-version 3.7 \
--machine-type n1-standard-4 \
--explanation-method integrated-gradients \
--num-integral-steps 25 \
--region=$REGION

# Make sure the IG model deployed correctly. State should be `READY` in the following log
! gcloud ai-platform versions describe $IG_VERSION --model $MODEL --region=$REGION

"""
Explanation: Create explainable model versions
For image models, we offer two choices for explanation methods: 
* Integrated Gradients (IG)
* XRAI 
You can find more info on each method in the documentation. You can deploy a version with both so that you can compare results. If you already know which explanation method you'd like to use, you can deploy one version and skip the code blocks for the other method.
Creating the version will take ~5-10 minutes. Note that your first deploy may take longer.
Deploy an Integrated Gradients model
End of explanation
"""


# Each time you create a version the name should be unique
XRAI_VERSION = 'v_xrai'

# Create the XRAI version with gcloud
! gcloud beta ai-platform versions create $XRAI_VERSION \
--model $MODEL \
--origin $export_path \
--runtime-version 2.3 \
--framework TENSORFLOW \
--python-version 3.7 \
--machine-type n1-standard-4 \
--explanation-method xrai \
--num-integral-steps 25 \
--region=$REGION

# Make sure the XRAI model deployed correctly. State should be `READY` in the following log
! gcloud ai-platform versions describe $XRAI_VERSION --model $MODEL  --region=$REGION

"""
Explanation: Deploy an XRAI model
End of explanation
"""


# Download test flowers from public bucket
! mkdir flowers
! gsutil -m cp gs://flowers_model/test_flowers/* ./flowers

# Resize the images to what your model is expecting (192,192)
test_filenames = []

for i in os.listdir('flowers'):
    img_path = 'flowers/' + i
    with PIL.Image.open(img_path) as ex_img:
        resize_img = ex_img.resize([192, 192])
        resize_img.save(img_path)
        test_filenames.append(img_path)

"""
Explanation: Get predictions and explanations
This section shows how to prepare test images to send to your deployed model, and how to send a batch prediction request to AI Explanations.
Get and prepare test images
To prepare the test images:

Download a small sample of images from the flowers dataset -- just enough for a batch prediction.
Resize the images to match the input shape (192, 192) of the model.
Save the resized images back to your bucket.
End of explanation
"""


# Prepare your images to send to your Cloud model
instances = []

for image_path in test_filenames:
    img_bytes = tf.io.read_file(image_path)
    b64str = b64encode(img_bytes.numpy()).decode('utf-8')
    instances.append({preprocess_name: {'b64': b64str}})

"""
Explanation: Format your explanation request
Prepare a batch of instances.
End of explanation
"""


# IG EXPLANATIONS
remote_ig_model = explainable_ai_sdk.load_model_from_ai_platform(project=PROJECT_ID, 
                                                                 model=MODEL, 
                                                                 version=IG_VERSION, 
                                                                 region=REGION)
ig_response = remote_ig_model.explain(instances)

for response in ig_response:
    response.visualize_attributions()

# XRAI EXPLANATIONS
remote_xrai_model = explainable_ai_sdk.load_model_from_ai_platform(project=PROJECT_ID, 
                                                                   model=MODEL, 
                                                                   version=XRAI_VERSION, 
                                                                   region=REGION)
xrai_response = remote_xrai_model.explain(instances)

for response in xrai_response:
    response.visualize_attributions()

"""
Explanation: Send the explanations request and visualize
If you deployed both an IG and an XRAI model, you can request explanations for both models and compare the results.
If you only deployed one model above, run only the cell for that explanation method.
You can use the Explainable AI SDK to send explanation requests to your deployed model and visualize the explanations.
End of explanation
"""


for i, response in enumerate(ig_response):
    attr = response.get_attribution()
    baseline_score = attr.baseline_score
    predicted_score = attr.example_score
    print('Baseline score: ', baseline_score)
    print('Predicted score: ', predicted_score)
    print('Predicted - Baseline: ', predicted_score - baseline_score, '\n')

"""
Explanation: Check explanations and baselines
To better make sense of your feature attributions, you can compare them with your model's baseline. For image models, the baseline_score returned by AI Explanations is the score your model would give an image input with the baseline you specified. The baseline is different for each class in the model. Every time your model predicts tulip as the top class, you'll see the same baseline score.
Earlier, you used a baseline image of np.random randomly generated values. If you'd like the baseline for your model to be solid black and white images instead, pass [0,1] as the value to input_baselines in your explanation_metadata.json file above. If the baseline_score is very close to the value of example_score, the highlighted pixels may not be meaningful.
Calculate the difference between baseline_score and example_score for the three test images above.
Note that the score values for classification models are probabilities: the confidence your model has in its predicted class. A score of 0.90 for tulip means your model has classified the image as a tulip with 90% confidence.
The code below checks baselines for the IG model. To inspect your XRAI model, swap out the ig_response and IG_VERSION variables below.
End of explanation
"""


# Convert your baseline from above to a base64 string
rand_test_img = PIL.Image.fromarray((random_baseline * 255).astype('uint8'))
buffer = io.BytesIO()
rand_test_img.save(buffer, format="PNG")
new_image_string = b64encode(np.asarray(buffer.getvalue())).decode("utf-8")

# Preview it
plt.imshow(rand_test_img)

sanity_check_img = {preprocess_name: {'b64': new_image_string}}

"""
Explanation: Explain the baseline image
Another way to check your baseline choice is to view explanations for this model's baseline image: an image array of randomly generated values using np.random. First, convert the same np.random baseline array generated earlier to a base64 string and preview it. This encodes the random noise as if it's a PNG image. Additionally, you must convert the byte buffer to a numpy array, because this is the format the underlying model expects for input when you send the explain request.
End of explanation
"""


# Sanity Check explanations EXPLANATIONS
sanity_check_response = remote_ig_model.explain([sanity_check_img])

"""
Explanation: Send the explanation request for the baseline image. (To check a baseline image for XRAI, change IG_VERSION to XRAI_VERSION below.)
End of explanation
"""


sanity_check_response[0].visualize_attributions()

"""
Explanation: Visualize the explanation for your random baseline image, highlighting the pixels that contributed to the prediction
End of explanation
"""


attr = sanity_check_response[0].get_attribution()
baseline_score = attr.baseline_score
example_score = attr.example_score

print(abs(baseline_score - example_score))

"""
Explanation: The difference between your model's predicted score and the baseline score for this image should be close to 0. Run the following cell to confirm. If there is a difference between these two values, try increasing the number of integral steps used when you deploy your model.
End of explanation
"""


# Delete model version resource
! gcloud ai-platform versions delete $IG_VERSION --quiet --model $MODEL
! gcloud ai-platform versions delete $XRAI_VERSION --quiet --model $MODEL

# Delete model resource
! gcloud ai-platform models delete $MODEL --quiet

# Delete Cloud Storage objects that were created
! gsutil -m rm -r gs://$BUCKET_NAME

"""
Explanation: Cleaning up
To clean up all GCP resources used in this project, you can delete the GCP
project you used for the tutorial.
Alternatively, you can clean up individual resources by running the following
commands:
End of explanation
"""


# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Explanation: If your Cloud Storage bucket doesn't contain any other objects and you would like to delete it, run gsutil rm -r gs://$BUCKET_NAME.
What's next?
To learn more about AI Explanations, check out the resources here.

AI Explanations documentation
AI Explanations whitepaper
Integrated gradients paper
XRAI paper

License
End of explanation
"""


