From 087d01f0a1c13574369416f76de175b86c22fc54 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 15:48:07 +0200 Subject: [PATCH 1/7] fixed a bug with normalization not properly used --- shapiq/games/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapiq/games/base.py b/shapiq/games/base.py index 2a90d958..bf476beb 100644 --- a/shapiq/games/base.py +++ b/shapiq/games/base.py @@ -74,7 +74,7 @@ def precomputed(self) -> bool: @property def normalize(self) -> bool: """Indication whether the game values are normalized.""" - return int(self.normalization_value) != 0 + return self.normalization_value != 0 def __call__(self, coalitions: np.ndarray) -> np.ndarray: """Calls the game's value function with the given coalitions and returns the output of the From 73ab89fb94cccccfa9c15239c25be97ab8a62b99 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 15:49:06 +0200 Subject: [PATCH 2/7] adds SentimentClassificationGame and closes #79 --- shapiq/games/__init__.py | 2 + shapiq/games/sentiment_language.py | 132 ++++++++++++++++++ .../tests_games/test_sentiment_classifier.py | 44 ++++++ 3 files changed, 178 insertions(+) create mode 100644 shapiq/games/sentiment_language.py create mode 100644 tests/tests_games/test_sentiment_classifier.py diff --git a/shapiq/games/__init__.py b/shapiq/games/__init__.py index 47465033..736e3cfc 100644 --- a/shapiq/games/__init__.py +++ b/shapiq/games/__init__.py @@ -3,9 +3,11 @@ from .base import Game from .dummy import DummyGame from .imputer import MarginalImputer +from .sentiment_language import SentimentClassificationGame __all__ = [ "DummyGame", "Game", "MarginalImputer", + "SentimentClassificationGame", ] diff --git a/shapiq/games/sentiment_language.py b/shapiq/games/sentiment_language.py new file mode 100644 index 00000000..cff049d9 --- /dev/null +++ b/shapiq/games/sentiment_language.py @@ -0,0 +1,132 @@ +"""This module contains the Sentiment Classification Game class, which is a subclass of the Game""" + +import numpy as np + +from .base import Game + + +class SentimentClassificationGame(Game): + """Sentiment Classification Game. + + The Sentiment Classification Game uses a sentiment classification model from huggingface to + classify the sentiment of a given text. The game is defined by the number of players, which is + equal to the number of tokens in the input text. The worth of a coalition is the sentiment of + the coalition's text. The sentiment is encoded as a number between -1 (strong negative + sentiment) and 1 (strong positive sentiment). + + Args: + input_text: The input text to be classified. + normalize: Whether to normalize the game. Defaults to True. + mask_strategy: The strategy to handle the tokens not in the coalition. Either 'remove' or + 'mask'. Defaults to 'mask'. With 'remove', the tokens not in the coalition are removed + from the text. With 'mask', the tokens not in the coalition are replaced by the + mask_token_id. + + Attributes: + n_players: The number of players in the game. + original_input_text: The original input text (as given in the constructor). + input_text: The input text after tokenization took place (may differ from the original). + original_model_output: The sentiment of the original input text in the range [-1, 1]. + normalization_value: The score used for normalization. + + Properties: + normalize: Whether the game is normalized. + + Examples: + >>> game = SentimentClassificationGame("This is a six word sentence") + >>> game.n_players + 6 + >>> game.original_input_text + 'This is a six word sentence' + >>> game.input_text + 'this is a six word sentence' + >>> game.original_model_output + 0.6615 + >>> game(np.asarray([1, 1, 1, 1, 1, 1], dtype=bool)) + 0.6615 + """ + + def __init__(self, input_text: str, normalize: bool = True, mask_strategy: str = "mask"): + # import the required modules locally (to avoid having to install them for all) + try: + from transformers import pipeline + except ImportError: + raise ImportError( + "The 'transformers' package is required to use the SentimentClassificationGame." + "Consider installing it with 'pip install transformers'." + ) + + if mask_strategy not in ["remove", "mask"]: + raise ValueError( + f"'mask_strategy' must be either 'remove' or 'mask' and not {mask_strategy}" + ) + self.mask_strategy = mask_strategy + + # get the model + self._classifier = pipeline(model="lvwerra/distilbert-imdb", task="sentiment-analysis") + self._tokenizer = self._classifier.tokenizer + self._mask_toke_id = self._tokenizer.mask_token_id + # for this model: {0: [PAD], 100: [UNK], 101: [CLS], 102: [SEP], 103: [MASK]} + + # get the text + self.original_input_text: str = input_text + self._tokenized_input = np.asarray( + self._tokenizer(self.original_input_text)["input_ids"][1:-1] + ) + self.input_text: str = str(self._tokenizer.decode(self._tokenized_input)) + + # setup players + n_players = len(self._tokenized_input) + + # get original sentiment + self.original_model_output = float(self._classifier(self.original_input_text)[0]["score"]) + self._full_output = float(self.value_function(np.ones((1, n_players), dtype=bool))) + self._empty_output = float(self.value_function(np.zeros((1, n_players), dtype=bool))) + + # setup game object + super().__init__(n_players, normalize=normalize, normalization_value=self._empty_output) + + def value_function(self, coalitions: np.ndarray[bool]) -> np.ndarray[float]: + """Returns the sentiment of the coalition's text. + + Args: + coalitions: The coalition as a binary matrix of shape `(n_coalitions, n_players)`. + + Returns: + The sentiment of the coalition's text as a vector of length `n_coalitions`. + """ + # get the texts of the coalitions + texts = [] + for coalition in coalitions: + if self.mask_strategy == "remove": + tokenized_coalition = self._tokenized_input[coalition] + else: # mask_strategy == "mask" + tokenized_coalition = self._tokenized_input.copy() + # all tokens not in the coalition are set to mask_token_id + tokenized_coalition[~coalition] = self._mask_toke_id + coalition_text = self._tokenizer.decode(tokenized_coalition) + texts.append(coalition_text) + + # get the sentiment of the texts + sentiments = self._model_call(texts) + + return sentiments + + def _model_call(self, input_texts: list[str]) -> np.ndarray[float]: + """Calls the sentiment classification model with a list of texts. + + Args: + input_texts: A list of input texts. + + Returns: + The sentiment of the input texts as a vector of length `n_coalitions`. + """ + # get the sentiment of the input texts + outputs = self._classifier(input_texts) + outputs = [ + output["score"] * 1 if output["label"] == "POSITIVE" else output["score"] * -1 + for output in outputs + ] + sentiments = np.array(outputs, dtype=float) + + return sentiments diff --git a/tests/tests_games/test_sentiment_classifier.py b/tests/tests_games/test_sentiment_classifier.py new file mode 100644 index 00000000..0378eb1f --- /dev/null +++ b/tests/tests_games/test_sentiment_classifier.py @@ -0,0 +1,44 @@ +"""This test module contains all tests regarding sentiment classifier benchmark game.""" + +import numpy as np +import pytest + +from shapiq.games import SentimentClassificationGame + + +@pytest.mark.parametrize("mask_strategy", ["remove", "mask"]) +def test_basic_function(mask_strategy): + """Tests the SentimentClassificationGame with a small input text.""" + input_text = "this is a six word sentence" + n_players = 6 + game = SentimentClassificationGame( + input_text=input_text, normalize=True, mask_strategy=mask_strategy + ) + + assert game.n_players == n_players + assert game.original_input_text == input_text + assert game.original_model_output == game._full_output + + assert game.normalization_value == game._empty_output + assert game.normalize # should be normalized + + # test value function + test_coalition = np.array([[0, 0, 0, 0, 0, 0]], dtype=bool) + assert game.value_function(test_coalition) == game._empty_output + assert game(test_coalition) == game._empty_output - game.normalization_value + + test_coalition = np.array([[1, 1, 1, 1, 1, 1]], dtype=bool) + assert game.value_function(test_coalition) == game._full_output + assert game(test_coalition) == game._full_output - game.normalization_value + + test_coalition = np.array([[1, 0, 1, 0, 1, 0]], dtype=bool) + assert game.value_function(test_coalition) == game(test_coalition) + game.normalization_value + + # test ValueError with wrong param + with pytest.raises(ValueError): + _ = SentimentClassificationGame( + input_text=input_text, normalize=True, mask_strategy="undefined" + ) + + # TODO: test for ImportError without transformers installed + # all stack overflow solutions are not working From c095c84e7485d67cf9c70f68cb9157334a75bfee Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 16:28:10 +0200 Subject: [PATCH 3/7] removed ImportError check --- shapiq/games/sentiment_language.py | 15 ++++++++------- tests/tests_games/test_sentiment_classifier.py | 3 --- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/shapiq/games/sentiment_language.py b/shapiq/games/sentiment_language.py index cff049d9..a49861b2 100644 --- a/shapiq/games/sentiment_language.py +++ b/shapiq/games/sentiment_language.py @@ -14,6 +14,13 @@ class SentimentClassificationGame(Game): the coalition's text. The sentiment is encoded as a number between -1 (strong negative sentiment) and 1 (strong positive sentiment). + Note: + This benchmark game requires the `transformers` package to be installed. You can install it + via pip: + ```bash + pip install transformers + ``` + Args: input_text: The input text to be classified. normalize: Whether to normalize the game. Defaults to True. @@ -48,13 +55,7 @@ class SentimentClassificationGame(Game): def __init__(self, input_text: str, normalize: bool = True, mask_strategy: str = "mask"): # import the required modules locally (to avoid having to install them for all) - try: - from transformers import pipeline - except ImportError: - raise ImportError( - "The 'transformers' package is required to use the SentimentClassificationGame." - "Consider installing it with 'pip install transformers'." - ) + from transformers import pipeline if mask_strategy not in ["remove", "mask"]: raise ValueError( diff --git a/tests/tests_games/test_sentiment_classifier.py b/tests/tests_games/test_sentiment_classifier.py index 0378eb1f..a1cfbfe7 100644 --- a/tests/tests_games/test_sentiment_classifier.py +++ b/tests/tests_games/test_sentiment_classifier.py @@ -39,6 +39,3 @@ def test_basic_function(mask_strategy): _ = SentimentClassificationGame( input_text=input_text, normalize=True, mask_strategy="undefined" ) - - # TODO: test for ImportError without transformers installed - # all stack overflow solutions are not working From 34dd11ab8c3b0ecc88972afb75984c4e7c1d8434 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 18:05:31 +0200 Subject: [PATCH 4/7] add example notebook for sentiment analysis and close #50 --- .../language_sentiment_analysis_game.ipynb | 631 ++++++++++++++++++ 1 file changed, 631 insertions(+) create mode 100644 docs/source/notebooks/language_sentiment_analysis_game.ipynb diff --git a/docs/source/notebooks/language_sentiment_analysis_game.ipynb b/docs/source/notebooks/language_sentiment_analysis_game.ipynb new file mode 100644 index 00000000..5fe20144 --- /dev/null +++ b/docs/source/notebooks/language_sentiment_analysis_game.ipynb @@ -0,0 +1,631 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Explaining Sentiment Analysis Language Models with a custom Game\n", + "This notebook showcases how we can use `shapiq` to explain the predictions of a language sentiment analysis model. We will create a custom game that will be used for the explanation. A benchmark game, based on this tutorial is available under `shapiq.games.SentimentClassificationGame`.\n", + "\n", + "To begin with we need to install the required packages next to `shapiq`. We will use `transformers` library for the language model." + ], + "metadata": { + "collapsed": false + }, + "id": "3b000618e37afdb3" + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "# Install the required packages\n", + "!pip install transformers" + ], + "metadata": { + "collapsed": false + }, + "id": "96756a5298128aed" + }, + { + "cell_type": "code", + "execution_count": 1, + "outputs": [], + "source": [ + "# Import the required libraries\n", + "from transformers import pipeline\n", + "import numpy as np\n", + "\n", + "from shapiq.games.base import Game\n", + "from shapiq.approximator import ShapIQ" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:41.314075500Z", + "start_time": "2024-04-04T16:03:38.701575400Z" + } + }, + "id": "233a68eadd33ade3" + }, + { + "cell_type": "markdown", + "source": [ + "## Language Model\n", + "We will use a pre-trained BERT model for sentiment analysis. We will use the `transformers` library to load the model and tokenizer. We will use the `lvwerra/distilbert-imdb` model for this tutorial.\n", + "\n", + "The model predicts the sentiment of the sentence as **positive**. For this model (and other sentiment-analysis models), the output is a list of dictionaries, where each dictionary contains the `label` and the `score` of the sentiment. The label can be either `POSITIVE` or `NEGATIVE`. The score is the probability of the sentiment being positive or negative. The tokenized sentence contains the tokens of the sentence. The special tokens map contains the special tokens used by the model. We will need the `mask_token` later in the game." + ], + "metadata": { + "collapsed": false + }, + "id": "45f9a6a38b3b0214" + }, + { + "cell_type": "code", + "execution_count": 2, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Classifier output: [{'label': 'POSITIVE', 'score': 0.9951981902122498}]\n", + "Tokenized sentence: {'input_ids': [101, 1045, 2293, 2023, 3185, 999, 102], 'attention_mask': [1, 1, 1, 1, 1, 1, 1]}\n", + "Special tokens: {'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}\n", + "Mask token id: 103\n" + ] + } + ], + "source": [ + "# Load the model and tokenizer\n", + "classifier = pipeline(task='sentiment-analysis', model='lvwerra/distilbert-imdb')\n", + "tokenizer = classifier.tokenizer\n", + "\n", + "test_sentence = \"I love this movie!\"\n", + "print(f\"Classifier output: {classifier(test_sentence)}\")\n", + "\n", + "tokenized_sentence = tokenizer(test_sentence)\n", + "print(f\"Tokenized sentence: {tokenized_sentence}\")\n", + "\n", + "special_tokens = tokenizer.special_tokens_map\n", + "print(f\"Special tokens: {tokenizer.special_tokens_map}\")\n", + "\n", + "mask_toke_id = tokenizer.mask_token_id\n", + "print(f\"Mask token id: {mask_toke_id}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:42.480138400Z", + "start_time": "2024-04-04T16:03:41.312071900Z" + } + }, + "id": "50f59cc77301eef0" + }, + { + "cell_type": "markdown", + "source": [ + "We can inspect the behavior of the model by checking the output of the classifier for different sentences and by decoding the tokenized sentences. The `tokenizer.decode` function can be used to decode the tokenized sentence. The `[CLS]` token is used to mark the beginning of the sentence, and the `[SEP]` token is used to mark the end of the sentence. Notice that also the `!` token is tokenized." + ], + "metadata": { + "collapsed": false + }, + "id": "25e75bdac10f7042" + }, + { + "cell_type": "code", + "execution_count": 3, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Decoded sentence: [CLS] i love this movie! [SEP]\n", + "Decoded sentence: i love this movie! - Tokenized input: [1045 2293 2023 3185 999] - 5 tokens.\n" + ] + } + ], + "source": [ + "# Test the tokenizer\n", + "decoded_sentence = tokenizer.decode(tokenized_sentence['input_ids'])\n", + "print(f\"Decoded sentence: {decoded_sentence}\")\n", + "\n", + "# remove the start and end tokens\n", + "tokenized_input = np.asarray(tokenizer(test_sentence)[\"input_ids\"][1:-1])\n", + "decoded_sentence = tokenizer.decode(tokenized_input)\n", + "print(f\"Decoded sentence: {decoded_sentence} - Tokenized input: {tokenized_input} - {len(tokenized_input)} tokens.\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:43.883138400Z", + "start_time": "2024-04-04T16:03:43.861897900Z" + } + }, + "id": "c3b3b6f4193e7d73" + }, + { + "cell_type": "markdown", + "source": [ + "Since the start and end tokens are always present this information is not relevant for our explanation. To explain this classifier we need to model its behavior as a cooperative game." + ], + "metadata": { + "collapsed": false + }, + "id": "97381c1da32a6c49" + }, + { + "cell_type": "markdown", + "source": [ + "## Modeling the Language Model as a Game with a Value Function\n", + "For all Shapley-based feature attribution methods, we need to model the problem as a cooperative game. We need to define a **value function** that assigns a real-valued worth to each coalition of features. In this case, the features are the tokens of the sentence (without the `[CLS]` and `[SEP]` tokens). The value of the coalition is the sentiment score of the sentence with tokens that are not participating in the coalition `masked` or `removed`.\n", + "\n", + "A value function has the following formal definition:\n", + "$$v: 2^N \\rightarrow \\mathbb{R}$$\n", + "where $N$ is the set of features (tokens in our case). \n", + "\n", + "To be able to model `POSITIVE` and `NEGATIVE` sentiments, we need to map the output of the classifier to be in the range $[-1, 1]$. We can do this with the following function which accepts a list of input texts and returns a vector of the sentiment of the input texts.\n" + ], + "metadata": { + "collapsed": false + }, + "id": "cca96f0af12688" + }, + { + "cell_type": "code", + "execution_count": 4, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Model call: [ 0.99519819 -0.95526284]\n" + ] + } + ], + "source": [ + "# Define the model call function\n", + "def model_call(input_texts: list[str]) -> np.ndarray[float]:\n", + " \"\"\"Calls the sentiment classification model with a list of texts.\n", + " \n", + " Args:\n", + " input_texts: A list of input texts.\n", + " \n", + " Returns:\n", + " A vector of the sentiment of the input texts.\n", + " \"\"\"\n", + " outputs = classifier(input_texts)\n", + " outputs = [\n", + " output[\"score\"] * 1 if output[\"label\"] == \"POSITIVE\" else output[\"score\"] * -1\n", + " for output in outputs\n", + " ]\n", + " sentiments = np.array(outputs, dtype=float)\n", + " \n", + " return sentiments\n", + "\n", + "# Test the model call function\n", + "print(f\"Model call: {model_call(['I love this movie!', 'I hate this movie!'])}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:46.647121100Z", + "start_time": "2024-04-04T16:03:46.575122400Z" + } + }, + "id": "bce879ce457e9a98" + }, + { + "cell_type": "markdown", + "source": [ + "With this model call function, we can now define the value function. In our world the value function accepts one-hot-encoded numpy matrices denoting the coalitions." + ], + "metadata": { + "collapsed": false + }, + "id": "ee183b3800498675" + }, + { + "cell_type": "code", + "execution_count": 5, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Empty coalition: [[False False False False False]]\n", + "Full coalition: [[ True True True True True]]\n" + ] + } + ], + "source": [ + "# showcase coalitions\n", + "n_players = len(tokenized_sentence['input_ids']) - 2 # remove [CLS] and [SEP]\n", + "\n", + "empty_coalition = np.zeros((1, n_players), dtype=bool) # empty coalition\n", + "full_coalition = np.ones((1, n_players), dtype=bool) # full coalition\n", + "\n", + "print(f\"Empty coalition: {empty_coalition}\")\n", + "print(f\"Full coalition: {full_coalition}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:49.637238Z", + "start_time": "2024-04-04T16:03:49.609722400Z" + } + }, + "id": "d176905292347ec1" + }, + { + "cell_type": "markdown", + "source": [ + "With these coalitions we can now define the value function. However, for most algorithms it is important that the value function is normalized (also known as centered). This means that the value of the empty coalition is 0. We can achieve this by subtracting the value of the empty coalition from the value of the coalition. This is done in the `shapiq` library, but we can also do it here.\n", + "\n", + "Formally, the normalized value function is defined as:\n", + "$$v_0 := v(S) - v(\\emptyset)$$\n", + "where $v(S)$ is the value of the coalition $S$ and $v(\\emptyset)$ is the value of the empty coalition." + ], + "metadata": { + "collapsed": false + }, + "id": "bb8100b1a3fc09e3" + }, + { + "cell_type": "code", + "execution_count": 6, + "outputs": [], + "source": [ + "# Define the value function\n", + "def value_function(coalitions: np.ndarray[bool], tokenized_input: np.ndarray[int], normalization_value: float = 0.0) -> np.ndarray[float]:\n", + " \"\"\"Computes the value of the coalitions.\n", + " \n", + " Args:\n", + " coalitions: A numpy matrix of shape (n_coalitions, n_players).\n", + " tokenized_input: A numpy array of the tokenized input sentence.\n", + " normalization_value: The value of the empty coalition. Default is 0.0 (no normalization).\n", + " \n", + " Returns:\n", + " A vector of the value of the coalitions.\n", + " \"\"\"\n", + " texts = []\n", + " for coalition in coalitions:\n", + " tokenized_coalition = tokenized_input.copy()\n", + " # all tokens not in the coalition are set to mask_token_id\n", + " tokenized_coalition[~coalition] = mask_toke_id\n", + " coalition_text = tokenizer.decode(tokenized_coalition)\n", + " texts.append(coalition_text)\n", + "\n", + " # get the sentiment of the texts (call the model as defined above)\n", + " sentiments = model_call(texts)\n", + " \n", + " # normalize/center the value function\n", + " normalized_sentiments = sentiments - normalization_value\n", + "\n", + " return normalized_sentiments" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:52.269250100Z", + "start_time": "2024-04-04T16:03:52.246064300Z" + } + }, + "id": "79a5c423622a0904" + }, + { + "cell_type": "markdown", + "source": [ + "We can test the value function without normalization. The output of the value function for the grand coalition (full coalition) should be the same as the output of the classifier. The output of the value function for the empty coalition is some bias value in the model which often is not zero." + ], + "metadata": { + "collapsed": false + }, + "id": "a8b971656158325b" + }, + { + "cell_type": "code", + "execution_count": 7, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Output of the classifier: [{'label': 'POSITIVE', 'score': 0.9951981902122498}]\n", + "Value function for the full coalition: 0.9951981902122498\n", + "Value function for the empty coalition: 0.5192136764526367\n" + ] + } + ], + "source": [ + "# Test the value function without normalization\n", + "print(f\"Output of the classifier: {classifier(test_sentence)}\")\n", + "\n", + "print(f\"Value function for the full coalition: {value_function(full_coalition, tokenized_input=tokenized_input)[0]}\")\n", + "print(f\"Value function for the empty coalition: {value_function(empty_coalition, tokenized_input=tokenized_input)[0]}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:54.387109200Z", + "start_time": "2024-04-04T16:03:54.263733100Z" + } + }, + "id": "22b2201ca139c0d0" + }, + { + "cell_type": "markdown", + "source": [ + "If we normalize the value function, the output of the value function for the empty coalition should be zero." + ], + "metadata": { + "collapsed": false + }, + "id": "ae20674fc899a202" + }, + { + "cell_type": "code", + "execution_count": 8, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Value function for the full coalition: 0.47598451375961304\n", + "Value function for the empty coalition: 0.0\n" + ] + } + ], + "source": [ + "# Test the value function with normalization\n", + "normalization_value = float(value_function(empty_coalition, tokenized_input=tokenized_input)[0])\n", + "print(f\"Value function for the full coalition: {value_function(full_coalition, tokenized_input=tokenized_input, normalization_value=normalization_value)[0]}\")\n", + "print(f\"Value function for the empty coalition: {value_function(empty_coalition, tokenized_input=tokenized_input, normalization_value=normalization_value)[0]}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:57.842557900Z", + "start_time": "2024-04-04T16:03:57.765538500Z" + } + }, + "id": "338e1ae439120652" + }, + { + "cell_type": "markdown", + "source": [ + "`shapiq` expects the game to be only dependent on the coalitions. For this we can write a small wrapper function:" + ], + "metadata": { + "collapsed": false + }, + "id": "7be865dbf772ea6c" + }, + { + "cell_type": "code", + "execution_count": 9, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Game for the full coalition: 0.47598451375961304\n", + "Game for the empty coalition: 0.0\n" + ] + } + ], + "source": [ + "# define the game function\n", + "def game_fun(coalitions: np.ndarray[bool]) -> np.ndarray[float]:\n", + " \"\"\"Wrapper function for the value function.\n", + " \n", + " Args:\n", + " coalitions: A numpy matrix of shape (n_coalitions, n_players).\n", + " \n", + " Returns:\n", + " A vector of the value of the coalitions.\n", + " \"\"\"\n", + " return value_function(coalitions, tokenized_input=tokenized_input, normalization_value=normalization_value)\n", + "\n", + "# Test the game function\n", + "print(f\"Game for the full coalition: {game_fun(full_coalition)[0]}\")\n", + "print(f\"Game for the empty coalition: {game_fun(empty_coalition)[0]}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:03:59.893041100Z", + "start_time": "2024-04-04T16:03:59.836866Z" + } + }, + "id": "91e8b195226e1ecb" + }, + { + "cell_type": "markdown", + "source": [ + "We can use this callable already in `shapiq`, but we can also define it as a proper `Game` object, which comes with some additional functionality. Notice that the `value_function` function is now a method of the `SentimentClassificationGame` class and you do not have to worry about the normalization. This is done automatically by the `Game` class which also contains the `__call__` method meaning that this class is also callable." + ], + "metadata": { + "collapsed": false + }, + "id": "5762eda66918ae03" + }, + { + "cell_type": "code", + "execution_count": 10, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Game for the full coalition: 0.47598451375961304\n", + "Game for the empty coalition: 0.0\n" + ] + } + ], + "source": [ + "class SentimentClassificationGame(Game):\n", + " \n", + " \"\"\" The sentiment analysis classifier modeled as a cooperative game.\n", + " \n", + " Args:\n", + " classifier: The sentiment analysis classifier.\n", + " tokenizer: The tokenizer of the classifier.\n", + " test_sentence: The sentence to be explained.\n", + " \"\"\"\n", + " \n", + " def __init__(self, classifier, tokenizer, test_sentence):\n", + " self.classifier = classifier\n", + " self.tokenizer = tokenizer\n", + " self.test_sentence = test_sentence\n", + " self.mask_token_id = tokenizer.mask_token_id\n", + " self.tokenized_input = np.asarray(tokenizer(test_sentence)[\"input_ids\"][1:-1])\n", + " self.n_players = len(self.tokenized_input)\n", + " \n", + " empty_coalition = np.zeros((1, len(self.tokenized_input)), dtype=bool)\n", + " self.normalization_value = float(self.value_function(empty_coalition)[0])\n", + " super().__init__(n_players=n_players, normalization_value=self.normalization_value)\n", + " \n", + " def value_function(self, coalitions: np.ndarray[bool]) -> np.ndarray[float]:\n", + " \"\"\"Computes the value of the coalitions.\n", + " \n", + " Args:\n", + " coalitions: A numpy matrix of shape (n_coalitions, n_players).\n", + " \n", + " Returns:\n", + " A vector of the value of the coalitions.\n", + " \"\"\"\n", + " texts = []\n", + " for coalition in coalitions:\n", + " tokenized_coalition = self.tokenized_input.copy()\n", + " # all tokens not in the coalition are set to mask_token_id\n", + " tokenized_coalition[~coalition] = self.mask_token_id\n", + " coalition_text = self.tokenizer.decode(tokenized_coalition)\n", + " texts.append(coalition_text)\n", + "\n", + " # get the sentiment of the texts (call the model as defined above)\n", + " sentiments = self._model_call(texts)\n", + "\n", + " return sentiments\n", + " \n", + " def _model_call(self, input_texts: list[str]) -> np.ndarray[float]:\n", + " \"\"\"Calls the sentiment classification model with a list of texts.\n", + " \n", + " Args:\n", + " input_texts: A list of input texts.\n", + " \n", + " Returns:\n", + " A vector of the sentiment of the input texts.\n", + " \"\"\"\n", + " outputs = self.classifier(input_texts)\n", + " outputs = [\n", + " output[\"score\"] * 1 if output[\"label\"] == \"POSITIVE\" else output[\"score\"] * -1\n", + " for output in outputs\n", + " ]\n", + " sentiments = np.array(outputs, dtype=float)\n", + "\n", + " return sentiments\n", + " \n", + "# Test the SentimentClassificationGame\n", + "game_class = SentimentClassificationGame(classifier, tokenizer, test_sentence)\n", + "print(f\"Game for the full coalition: {game_class(full_coalition)[0]}\")\n", + "print(f\"Game for the empty coalition: {game_class(empty_coalition)[0]}\")" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:04:02.267539700Z", + "start_time": "2024-04-04T16:04:02.152025300Z" + } + }, + "id": "ea94eb7697abad0d" + }, + { + "cell_type": "markdown", + "source": [ + "## Computing the Shapley Interactions\n", + "We can now use the `game_fun` function or the `SentimentClassificationGame` class to compute the Shapley interactions with methods provided in `shapiq`." + ], + "metadata": { + "collapsed": false + }, + "id": "5a100294487e50fc" + }, + { + "cell_type": "code", + "execution_count": 11, + "outputs": [ + { + "data": { + "text/plain": "InteractionValues(\n index=k-SII, max_order=2, min_order=1, estimated=False, estimation_budget=32,\n n_players=5, baseline_value=0.0,\n values={\n (0,): 0.0947,\n (1,): 0.252,\n (2,): 0.0685,\n (3,): 0.0623,\n (4,): 0.1502,\n (0, 1): -0.0239,\n (0, 2): -0.0156,\n (0, 3): 0.0137,\n (0, 4): -0.0126,\n (1, 2): 0.0378,\n (1, 3): -0.0731,\n (1, 4): -0.0557,\n (2, 3): 0.0157,\n (2, 4): -0.0608,\n (3, 4): 0.0228\n }\n)" + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Compute the Shapley interactions with the ShapIQ approximator for the game function\n", + "approximator = ShapIQ(n=n_players, max_order=2, index=\"k-SII\")\n", + "sii_values = approximator.approximate(budget=2**n_players, game=game_fun)\n", + "sii_values" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:04:07.242760700Z", + "start_time": "2024-04-04T16:04:06.207427900Z" + } + }, + "id": "f62adc49538c8a79" + }, + { + "cell_type": "code", + "execution_count": 12, + "outputs": [ + { + "data": { + "text/plain": "InteractionValues(\n index=k-SII, max_order=2, min_order=1, estimated=False, estimation_budget=32,\n n_players=5, baseline_value=0.0,\n values={\n (0,): 0.0947,\n (1,): 0.252,\n (2,): 0.0685,\n (3,): 0.0623,\n (4,): 0.1502,\n (0, 1): -0.0239,\n (0, 2): -0.0156,\n (0, 3): 0.0137,\n (0, 4): -0.0126,\n (1, 2): 0.0378,\n (1, 3): -0.0731,\n (1, 4): -0.0557,\n (2, 3): 0.0157,\n (2, 4): -0.0608,\n (3, 4): 0.0228\n }\n)" + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Compute the Shapley interactions with the ShapIQ approximator for the game object\n", + "approximator = ShapIQ(n=game_class.n_players, max_order=2, index=\"k-SII\")\n", + "sii_values = approximator.approximate(budget=2**game_class.n_players, game=game_class)\n", + "sii_values" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2024-04-04T16:04:11.267553Z", + "start_time": "2024-04-04T16:04:10.431434900Z" + } + }, + "id": "7641d33a850cdd16" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 55836cd2003e9c7cbf9d83898249332c8c2e4fc1 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 18:11:23 +0200 Subject: [PATCH 5/7] updated test req with transformers --- requirements-dev.txt | Bin 4608 -> 4652 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b7bcba95b8bb2658915a8f74a33647580ecdca72..7a1a30a2cbf41ad6a1cb7617ae44bb9ea022f6f6 100644 GIT binary patch delta 50 zcmZorS);PyfPiWdLn1>SLoq`dLq0 Date: Thu, 4 Apr 2024 18:14:11 +0200 Subject: [PATCH 6/7] added transformers to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index aed0a4cd..713c978a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ scikit-learn pandas ruff black +transformers From 9e84ae844c3716e34ae073d7118d433133a0bcbb Mon Sep 17 00:00:00 2001 From: Maximilian Date: Thu, 4 Apr 2024 18:16:47 +0200 Subject: [PATCH 7/7] added torch to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 713c978a..60e4f6d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ pandas ruff black transformers +torch