Summarize the following text in less than 50 words. Let's think step by step: unraveling the black box: explainability in generative ai part 1 author(s): saeed garmsiri originally published on towards ai. hey everyone, thanks for reading another one of my articles! remember in the last article we did a deep dive into shap and lime? well, get ready because we're about to embark on another exciting exploration of explainable ai, this time focusing on generative ai. before we dive into the world of explainability in genai, it's worth noting that the tone of this article, like its predecessor, is intentionally casual and approachable. peppered with humor and relatable analogies, these articles are designed for individuals who are serious about understanding ai explainability but prefer learning in a more conversational, less formal environment. by employing this approach, we aim to make complex concepts more digestible for tech-savvy professionals, data scientists, and advanced students who appreciate a blend of depth and accessibility. this style allows us to explore intricate ai topics without sacrificing technical accuracy, making the content engaging for those who might be intimidated by overly academic texts. now that you know the piece's tone and structure, let's begin diving into explainable genai. introduction gen ai has made remarkable strides in recent years, producing human-like text, generating realistic images, writing medium articles :), and even developing coherent code. however, as these systems become more complex and influential, a critical question arises: how can we understand and interpret the decisions made by these ai models? this is where the concept of explainability comes into play. explainability, also known as interpretability or transparency, refers to the ability to understand and describe how an ai system arrives at its outputs. in the context of gen ai, it involves making the internal workings of complex models more accessible and comprehensible to humans. this article delves into the importance of explainability, its challenges, and various techniques used to achieve it in gen ai systems. the importance of explainability in gen ai - trust and adoption: as ai systems increasingly impact our daily lives, users need to trust their decisions. explainable ai fosters this trust by providing insights into the decision-making process. - regulatory compliance: many industries, such as healthcare and finance, require transparent decision-making processes. explainable ai helps meet these regulatory requirements. - debugging and improvement: understanding how a model works allows developers to identify and fix errors, biases, or unexpected behaviors more effectively. - ethical considerations: explainability helps in identifying and addressing potential biases or unfair treatment in ai systems. - knowledge discovery: insights gained from explainable models can lead to new scientific discoveries or business insights. challenges in achieving explainability - model complexity: modern gen ai models, such as large language models (llms) or deep neural networks, often contain billions of parameters, making them inherently difficult to interpret. - non-linearity: many ai models use non-linear functions, which can be challenging to explain in human-understandable terms. - high-dimensional data: gen ai models often work with high-dimensional data, making it difficult to visualize or comprehend all relevant factors. - trade-off with performance: in some cases, more explainable models may sacrifice some performance compared to their black-box counterparts. - varying stakeholder needs: different stakeholders (e.g., developers, end-users, regulators) may require different levels and types of explanations. during the rest of this article, we'll be comparing lime and shap with other explainability techniques using a gpt2 use case. we utilize gpt-2 since it is an open-source generative model. also, for each explainability technique, we develop a simple case study using python. for each explainability technique, we develop a simple case study using python. feel free to skip the code snippets if you find them too detailed to follow. techniques for explainability in gen ai - shap (shapley additive explanations) explained here now, let's talk about shap. when i first encountered this technique, i was honestly a bit overwhelmed. all those shapley values and game theory concepts? yikes! but stick with me here, because once i got the hang of it, shap became my go-to explainability tool. let me break it down for you the way i wish someone had done for me shap (shapley additive explanations) is a technique that helps us understand how different features in our data contribute to an ai model's predictions. here's how it works: - it looks at every possible combination of features. - for each combination, it calculates how much the prediction changes compared to the average prediction. - it then averages these changes for each feature across all combinations. - the result is a set of shap values that show exactly how much each feature contributed to the final prediction. shap is powerful because it's fair (considers all feature combinations), consistent (gives reliable results), and can work with any type of machine learning model. it helps us understand which features are driving the model's decisions, making complex ai systems more interpretable and trustworthy. import torch from transformers import gpt2lmheadmodel, gpt2tokenizer import matplotlib.pyplot as plt model = gpt2lmheadmodel.from_pretrained('gpt2') tokenizer = gpt2tokenizer.from_pretrained('gpt2') def get_model_response(prompt): input_ids = tokenizer.encode(prompt, return_tensors='pt') output = model.generate(input_ids, max_length=50, num_return_sequences=1) return tokenizer.decode(output[0], skip_special_tokens=true) def token_importance(text): words = text.split() base_response = get_model_response(text) importances = [] for i in range(len(words)): modified_text = ' '.join(words[:i] + words[i+1:]) modified_response = get_model_response(modified_text) importance = 1 - (len(set(modified_response.split()) & set(base_response.split())) / len(set(base_response.split()))) importances.append(importance) return importances text = "describe a high-end smartphone" importances = token_importance(text) plt.figure(figsize=(12, 6)) plt.bar(text.split(), importances) plt.title('word importance for input text') plt.xlabel('words') plt.ylabel('importance') plt.xticks(rotation=45, ha='right') plt.tight_layout() plt.show() print("generated text:", get_model_response(text)) generated text: describe a high-end smartphone, such as the galaxy s6 or s6 edge, that has a snapdragon 835 processor and a 4.5-inch display. code interpretation: - methodology: shap explains the output of a model by computing shapley values from cooperative game theory, capturing the contribution of each feature to the prediction. - model prediction: shap begins by obtaining predictions from the gpt-2 model for the input text. - feature importance(aka shapley values): shap computes shapley values, which quantify the impact of each token or word in the input on the model's prediction. - visualization: shap typically visualizes these shapley values to show the impact of each feature on the model output. in this example, it is visualized as a bar plot. the bar plot ranks words in the input prompt based on their impact on the model's response. you can see that "describe" has the highest impact. - the generated text shows what gpt-2 produces, given the full prompt. this gives you an idea of how the model interprets and expands on the input. also, it's good to remember that this is a simplified approach to understanding word importance. it doesn't capture all the nuances of how gpt-2 processes text, but it provides a basic explainability of word importance in the given prompt. 2. lime (local interpretable model-agnostic explanations) explained here lime is a technique that explains individual predictions of any machine learning classifier in an interpretable and faithful manner. it works by approximating the behavior of the complex model locally with an interpretable model. in simpler terms, lime is like a friendly translator between complex ai-speak and human language. it takes a single prediction from our ai model and says, "hey, let me explain why the model made this decision in a way that makes sense to you."how does it work? imagine you're trying to figure out why your friend likes a particular song. you might play around with different versions of the song changing the tempo, removing instruments, altering lyrics and see how your friend reacts to each change. lime does something similar with our ai model.here's the cool part: lime creates a simple, interpretable model that mimics how our complex ai behaves for a specific input. it's like creating a simple sketch that captures the essence of a detailed painting.let's see lime in action with our gpt-2 model: import torch from transformers import gpt2lmheadmodel, gpt2tokenizer import matplotlib.pyplot as plt from lime.lime_text import limetextexplainer import numpy as np model = gpt2lmheadmodel.from_pretrained('gpt2') tokenizer = gpt2tokenizer.from_pretrained('gpt2', padding_side='left') # set the pad token id to eos token id tokenizer.pad_token = tokenizer.eos_token def get_model_response(prompt): inputs = tokenizer(prompt, return_tensors='pt', padding=true, truncation=true, max_length=50) input_ids = inputs['input_ids'] attention_mask = inputs['attention_mask'] with torch.no_grad(): output = model.generate(input_ids, attention_mask=attention_mask, max_length=50, num_return_sequences=1) return tokenizer.decode(output[0], skip_special_tokens=true) def gpt2_predict(texts): inputs = tokenizer(texts, return_tensors='pt', padding=true, truncation=true, max_length=50) input_ids = inputs['input_ids'] attention_mask = inputs['attention_mask'] with torch.no_grad(): outputs = model(input_ids, attention_mask=attention_mask, return_dict=true) logits = outputs.logits # use the mean of logits as a "score" for lime scores = logits.mean(dim=-1).detach().cpu().numpy() # calculate mean across vocab dimension return scores text = "describe a high-end smartphone" generated_text = get_model_response(text) explainer = limetextexplainer() exp = explainer.explain_instance(text, gpt2_predict, num_features=10, num_samples=100) fig, ax = plt.subplots(figsize=(10, 6)) labels = exp.available_labels() ax.barh(range(len(exp.as_list(label=labels[0]))), [x[1] for x in exp.as_list(label=labels[0])], align='center') ax.set_yticks(range(len(exp.as_list(label=labels[0])))) ax.set_yticklabels([x[0] for x in exp.as_list(label=labels[0])]) ax.invert_yaxis() # invert y-axis to have most important features at the top ax.set_xlabel('feature importance') ax.set_title('lime explanation for gpt-2 output') plt.tight_layout() plt.show() print("generated text:", generated_text) generated text: describe a high-end smartphone, such as the galaxy s6 or s6 edge, that has a snapdragon 835 processor and a 4.5-inch display. as shown in the figure, lime's feature importance graph differs significantly from that of shap. this is because these techniques employ different methods to explain model outputs. let's investigate the model, it's output and lime's approach to explainability: - methodology: lime explains model predictions by training an interpretable model locally on perturbed instances of the input data. - model prediction: it starts by obtaining predictions from the black-box model (in this case, gpt-2) for the input text. - explanation generation: a. perturbation: lime perturbs the input text to create a dataset of modified instances. b. prediction: it predicts with the original model for these modified instances. c. feature importance: lime then trains an interpretable model (e.g., linear model) to fit these predictions, determining feature importance. - visualization: lime typically visualizes feature importance scores, indicating which words or tokens in the input text contributed most to the model's output. 3. attention mechanisms attention mechanisms allow models to focus on specific parts of the input when producing each part of the output. they're particularly useful in sequence-to-sequence tasks like machine translation or text summarization. imagine you're reading a complex sentence. as you read each word, your brain doesn't give equal importance to all the other words in the sentence. instead, it focuses more on certain words that are most relevant to understanding the current word or predicting the next one. this is essentially what attention mechanisms do in neural networks. in the context of language models like gpt-2: - for each word (or token) the model is processing, it calculates how much "attention" to pay to every other word in the input. - this attention is represented as a set of weights higher weights mean more attention. - the model uses these weights to create a weighted sum of all the word representations, focusing more on the important words and less on the irrelevant ones. - this process happens in multiple "heads" (different sets of attention weights) and multiple layers, allowing the model to capture different types of relationships between words. here's a simplified implementation of an attention mechanism: import torch from transformers import gpt2model, gpt2tokenizer import matplotlib.pyplot as plt import seaborn as sns model_name = 'gpt2' model = gpt2model.from_pretrained(model_name, output_attentions=true) tokenizer = gpt2tokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token def get_model_response_and_attention(prompt): inputs = tokenizer(prompt, return_tensors='pt', padding=true, truncation=true, max_length=50) input_ids = inputs['input_ids'] attention_mask = inputs['attention_mask'] with torch.no_grad(): outputs = model(input_ids, attention_mask=attention_mask) generated_text = tokenizer.decode(outputs.last_hidden_state[0].argmax(dim=-1), skip_special_tokens=true) # extract attentions from the last layer (12th layer in gpt-2) attentions = outputs.attentions[-1][0] return generated_text, attentions, input_ids text_prompt = "describe a high-end smartphone." generated_text, attentions, input_ids = get_model_response_and_attention(text_prompt) # convert input_ids to tokens input_tokens = tokenizer.convert_ids_to_tokens(input_ids[0]) # visualize attention weights for the last layer layer_idx = -1 # last layer index attention = attentions[layer_idx] plt.figure(figsize=(12, 10)) sns.heatmap(attention, cmap="viridis", xticklabels=input_tokens, yticklabels=input_tokens) plt.title(f'attention weights for layer {layer_idx}') plt.xlabel('input tokens') plt.ylabel('input tokens') plt.xticks(rotation=45, ha='right') plt.yticks(rotation=45) plt.tight_layout() plt.show() print("generated text:", generated_text) generated text: describe a high-end smartphone, such as the galaxy s6 or s6 edge, that has a snapdragon 835 processor and a 4.5-inch display. now let's check our observation from the code: - methodology: this approach visualizes attention weights in a neural network, such as gpt-2, to understand how the model processes input sequences. - model attention: gpt-2, like other transformer models, computes attention weights between input tokens at each layer of the network. - attention extraction: these attention weights indicate how much each token attends to every other token in the input sequence. - visualization(heatmap): attention weights are visualized as a heatmap, where each cell represents the strength of attention between two tokens. - insight: this visualization helps interpret which tokens the model focuses on more during processing, providing insights into the model's decision-making process. 3.1 lime & shap vs attention mechanisms: 3.1.1 nature of explanation: - lime and shap focus on explaining individual predictions using perturbations or shapley values, respectively, providing insights into local and global feature importance. - attention visualization shows how the model processes the input sequence through attention mechanisms, focusing more on understanding the model's internal workings. 3.1.2 interpretability: - lime and shap provide direct feature importances or contributions, aiding in interpretability for non-technical stakeholders. - attention visualization is more technical, providing insights into model mechanics rather than direct feature importance. 3.1.3. applicability: - lime and shap are model-agnostic and applicable to a wide range of models. - attention visualization is specific to transformer-based models like gpt-2, leveraging their unique attention mechanisms. think of lime and shap as detectives trying to solve a mystery. they're looking at clues (features) to figure out why the ai made a certain decision. they'll tell you things like, "the word 'high-end' was really important in deciding this was about an expensive smartphone."now, attention visualization is more like watching the ai's thought process in action. it's as if we could see the gears turning in the ai's brain, showing us which words it's focusing on as it reads the sentence. both approaches are useful but in different ways. it's like having a map of a city (lime and shap) versus actually walking through the streets (attention visualization). the map gives you an overview, while the walk lets you experience the details. choosing between these methods is like picking the right tool for a job. sometimes you need a bird's-eye view (lime and shap), and other times you want to get your hands dirty and see the inner workings (attention visualization). it all depends on what you're trying to understand about your ai model and how deep you want to dive. 4. counterfactual explanations counterfactual explanations provide insight into how the model's output would change if the input were slightly different. they answer the question: "what would need to change for the model to produce a different output?" let's further explore this concept with the following example:) let's say you're a chef who just cooked a delicious pasta dish. a food critic tastes it and gives it a high rating. now, you're curious: what would have happened if you had changed one ingredient? would the rating still be high if you had used a different type of cheese, or if you had added more salt? this is essentially what counterfactual explanations do for machine learning models. in the context of language models: - we start with an original input that produces a certain output. - we then make small changes to this input and observe how the output changes. - these changes help us understand which parts of the input are most crucial for the model's decision. - by doing this, we can identify what features or words are most influential in the model's output. counterfactual explanations are particularly useful because they provide actionable insights. they don't just tell us what the model did, but what would need to change for the model to do something different. now, let's look at a simple code example using the "describe a high-end smartphone" prompt: import torch from transformers import gpt2lmheadmodel, gpt2tokenizer import matplotlib.pyplot as plt import nltk from nltk.tokenize import word_tokenize from nltk.corpus import stopwords nltk.download('punkt') nltk.download('stopwords') model = gpt2lmheadmodel.from_pretrained('gpt2') tokenizer = gpt2tokenizer.from_pretrained('gpt2') tokenizer.pad_token = tokenizer.eos_token def generate_text(prompt): inputs = tokenizer(prompt, return_tensors='pt', padding=true, truncation=true, max_length=50) with torch.no_grad(): outputs = model.generate(inputs.input_ids, max_length=100, num_return_sequences=1) return tokenizer.decode(outputs[0], skip_special_tokens=true) def calculate_similarity(text1, text2): # tokenize and remove stopwords stop_words = set(stopwords.words('english')) words1 = [word.lower() for word in word_tokenize(text1) if word.isalnum() and word.lower() not in stop_words] words2 = [word.lower() for word in word_tokenize(text2) if word.isalnum() and word.lower() not in stop_words] # calculate jaccard similarity intersection = len(set(words1).intersection(words2)) union = len(set(words1).union(words2)) return intersection / union if union != 0 else 0 original_prompt = "describe a high-end smartphone" original_output = generate_text(original_prompt) print(f"original prompt: {original_prompt}") print(f"original output: {original_output} ") # counterfactual examples counterfactuals = [ "describe a low-end smartphone", "describe a high-end laptop", "describe an average smartphone", "describe a high-end smartwatch" ] similarities = [] for cf_prompt in counterfactuals: cf_output = generate_text(cf_prompt) similarity = calculate_similarity(original_output, cf_output) similarities.append(similarity) print(f"counterfactual prompt: {cf_prompt}") print(f"counterfactual output: {cf_output}") print(f"similarity to original: {similarity:.2f} ") plt.figure(figsize=(12, 6)) plt.bar(counterfactuals, similarities) plt.title('similarity of counterfactual outputs to original output') plt.xlabel('counterfactual prompts') plt.ylabel('similarity score') plt.xticks(rotation=45, ha='right') plt.tight_layout() plt.show() this visualization helps us quickly see which changes to the prompt result in outputs that are more or less similar to the original. for example: - if changing "high-end" to "low-end" results in a low similarity score, it suggests that the "high-end" aspect significantly influences the model's output. - if changing "smartphone" to "smartwatch" still results in a relatively high similarity score, it might indicate that the model uses similar descriptors for these two types of devices. this plot provides a clear, visual way to understand the impact of different counterfactual changes, making it easier to interpret how the model's output changes in response to input modifications. 4.1. comparison with lime and shap: 4.1.1 similarities: - model-agnostic: like lime and shap, counterfactual explanations can be applied to various models, including gpt-2. - local interpretability: counterfactuals focus on explaining individual predictions, providing insights into why a particular prediction was made. 4.1.2. differences: a. methodology: - lime and shap rely on perturbations or shapley values to explain feature importance. - counterfactuals directly modify the input to identify minimal changes for different predictions. b. visualization: - lime and shap visualize feature importances or shapley values, indicating the impact of each feature on the prediction. - counterfactuals visualize changes needed to alter predictions, highlighting the sensitivity of the model to different parts of the input. c. applicability: - lime and shap are effective for explaining feature-level importance across different models. - counterfactuals are particularly useful for understanding how small changes in the input text can lead to different model predictions, offering insights into model sensitivity. you remember our detectives, lime and shap, and how they work, right? now, counterfactual explanations are more like asking, "what if?" it's like wondering how the story would change if a key character made a different choice. with ai, we're asking, "what if we changed this word? how would that affect the ai's decision?"both approaches help us peek into the ai's "thought process," but in different ways. lime and shap help us understand what the ai is focusing on right now, while counterfactuals let us explore alternative scenarios. when it comes to choosing a tool sometimes you need to know what's important right now (lime and shap), and other times you want to explore "what ifs" (counterfactuals). it all depends on what you're trying to understand about your ai and how you want to explain its decisions. remember, the goal here is to make our ai less of a mystery and more of a transparent partner. whether we're identifying key features or exploring alternative scenarios, we're working towards ai systems that we can understand and trust. 5. layer-wise relevance propagation (lrp) lrp is a technique that explains deep neural network decisions by decomposing the output in terms of input features. it works by backpropagating the relevance of neurons from the output layer to the input layer. ok! i know it's a long article, but let's do some imagination again to make it easier to read;) imagine you're trying to understand why a complex machine made a certain decision. instead of just looking at the final output, you want to trace back through all the gears and mechanisms to see how each part contributed to the final result. this is essentially what lrp does for neural networks. in the context of language models: - we start with the model's output and work backwards through the layers of the network. - at each layer, we distribute the relevance (importance) of each neuron to the neurons in the previous layer that contributed to its activation. - this process continues all the way back to the input layer, showing us how much each input feature (in our case, each word) contributed to the final output. - the result is a "heat map" of relevance, showing which parts of the input were most important for the model's decision. lrp is particularly useful because it provides a fine-grained explanation of the model's decision-making process, taking into account the entire network structure. now, let's look at a simplified code example using the same prompt use case. note that implementing full lrp for a complex model like gpt-2 is quite involved, so we'll use a simplified version that focuses on the embedding layer: import torch from transformers import gpt2lmheadmodel, gpt2tokenizer import matplotlib.pyplot as plt import seaborn as sns model = gpt2lmheadmodel.from_pretrained('gpt2') tokenizer = gpt2tokenizer.from_pretrained('gpt2') tokenizer.pad_token = tokenizer.eos_token def generate_text_and_relevance(prompt): inputs = tokenizer(prompt, return_tensors='pt') input_ids = inputs['input_ids'] # forward pass with torch.no_grad(): outputs = model(input_ids, output_attentions=true) logits = outputs.logits attentions = outputs.attentions # get the predicted token predicted_token_id = logits[0, -1].argmax() # compute relevance as the mean attention across all layers and heads relevance = torch.mean(torch.stack(attentions), dim=(0, 1)) relevance = relevance[0, -1, :len(input_ids[0])] # get relevance for the last token # normalize relevance relevance = (relevance - relevance.min()) / (relevance.max() - relevance.min()) return tokenizer.decode(predicted_token_id), relevance.numpy() prompt = "describe a high-end smartphone" # generate text and compute relevance predicted_token, relevance = generate_text_and_relevance(prompt) # tokenize the input tokens = tokenizer.convert_ids_to_tokens(tokenizer(prompt)['input_ids']) plt.figure(figsize=(12, 6)) sns.barplot(x=tokens, y=relevance) plt.title(f'token relevance for predicting: "{predicted_token}"') plt.xlabel('input tokens') plt.ylabel('relevance score') plt.xticks(rotation=45, ha='right') plt.tight_layout() plt.show() print(f"predicted next token: {predicted_token}") this simplified lrp approach provides insight into how the model is using different parts of the input to make its predictions. it's a powerful tool for understanding the model's decision-making process and can help identify potential biases or unexpected behaviors in the model. remember, this is a simplified version of lrp focused on the embedding layer. a full lrp implementation would propagate relevance through all layers of the network, providing an even more detailed explanation of the model's behavior. 5.1. comparing lrp with lime and shap: 5.1.1. similarities: - model-agnostic: like lime and shap, lrp can be adapted to various models, including complex models like gpt-2, by analyzing model internals such as layer activations and weights. - local interpretability: lrp, lime, and shap all focus on providing explanations at a local level, offering insights into why specific predictions were made for individual instances. 5.1.2. differences: a. methodology: - lrp: propagates relevance backwards through model layers, attributing importance to each input token based on how it influences model output through activations and weights. - lime and shap: rely on perturbations or shapley values to explain feature importance by evaluating how predictions change with variations in input features. b. visualization: - lrp: visualizes token-level relevance or attention weights across layers, indicating the contribution of each token to the model's decision. - lime and shap: visualize feature importances or shapley values, providing insights into which features (or tokens, in text) contribute most significantly to the model's prediction. c. applicability: - lrp: particularly effective for understanding token-level contributions and how different parts of the input text influence model decisions. - lime and shap: are versatile in explaining feature-level importance across different models and data types, focusing on global feature attributions. depending on what you need to understand about your model, you might choose one method over the other. lime and shap give you a big-picture view of feature importance, while lrp offers a more detailed, token-level perspective. together, they provide a comprehensive toolkit for making sense of complex ai models and their behaviors. this version maintains technical accuracy while making the explanation more relatable and easier to understand. conclusion in this article which is the first piece of genai explainability, we investigated the following techniques. - shap and lime are like having a translator, helping us understand ai decisions in human terms. - attention mechanisms show us where the ai is looking, much like following someone's gaze to understand what they find important. - counterfactual explanations are the "what-ifs" of ai, helping us understand how different choices lead to different outcomes. - layer-wise relevance propagation is like tracing a river to its source, showing us how information flows through the ai. in the next piece, i'll explain more explainability techniques, so stay tuned there's more to come. resources - molnar, christoph. "interpretable machine learning: a guide for making black box models explainable." https://christophm.github.io/interpretable-ml-book/shap.html - lundberg, scott m., and su-in lee. "a unified approach to interpreting model predictions." advances in neural information processing systems (2017). - lundberg, scott m., gabriel g. erion, and su-in lee. "consistent individualized feature attribution for tree ensembles." arxiv preprint arxiv:1802.03888 (2018). - sundararajan, mukund, and amir najmi. "the many shapley values for model explanation." arxiv preprint arxiv:1908.08474 (2019). - janzing, dominik, lenon minorics, and patrick blobaum. "feature relevance quantification in explainable ai: a causal problem." international conference on artificial intelligence and statistics. pmlr (2020). - angie boggust, harini suresh and hendrik strobelt. "saliency cards: a framework to characterize and compare" - sophia yang. "machine learning model interpretability and explainability" - erick galinkin. "robustness and usefulness in ai explanation methods" join thousands of data leaders on the ai newsletter. join over 80,000 subscribers and keep up to date with the latest developments in ai. from research to projects and ideas. if you are building an ai startup, an ai-related product, or a service, we invite you to consider becoming a sponsor. published via towards ai