Created using Colaboratory

This commit is contained in:
udlbook
2023-10-30 09:26:06 +00:00
parent fd1844ed44
commit fbf9ac0e40

View File

@@ -0,0 +1,412 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"authorship_tag": "ABX9TyOLMPuSWpvv8BfyPV36RuJP",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/github/udlbook/udlbook/blob/main/Notebooks/Chap21/21_2_Explainability.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"# **Notebook 21.2: Explainability**\n",
"\n",
"This notebook investigates the LIME explainability method as depicted in figure 21.3 of the book.\n",
"\n",
"Work through the cells below, running each cell in turn. In various places you will see the words \"TO DO\". Follow the instructions at these places and make predictions about what is going to happen or write code to complete the functions.\n",
"\n",
"Contact me at udlbookmail@gmail.com if you find any mistakes or have any suggestions.\n"
],
"metadata": {
"id": "t9vk9Elugvmi"
}
},
{
"cell_type": "code",
"source": [
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import numpy.matlib\n",
"from matplotlib.colors import ListedColormap"
],
"metadata": {
"id": "yC_LpiJqZXEL"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"First we'll build a black box model for predicting a credit score. This simulates a neural network. It takes four inputs $x1,x2,x3,x4$ in a column vector and it returns a value $y$. Let's assume that if the output $y$ is greater than 0 then you get the loan, and if the output is less than 0 then you don't get the zone."
],
"metadata": {
"id": "WM6mq9KNit3j"
}
},
{
"cell_type": "code",
"source": [
"# Details of this not important -- a hacky thing that takes four inputs and returns\n",
"# a scalar output\n",
"class BlackBoxModel:\n",
" def __init__(self):\n",
" self.n_dim = 4\n",
" self.n_points = 10\n",
" self.means = np.random.uniform(size=(self.n_dim, self.n_points))\n",
" self.stds = np.random.uniform(size=(self.n_dim,self.n_points))+0.1\n",
" self.values = np.random.normal(size=(self.n_points))/10\n",
" self.values = self.values - np.mean(self.values)\n",
"\n",
"\n",
" def intensity(self, x, mean, std, value):\n",
"\n",
" dist = (x-np.matlib.repmat(mean,1,x.shape[1])) / np.matlib.repmat(std,1,x.shape[1])\n",
" out = value * np.exp(-np.sum(dist*dist,axis=0))\n",
" out = out[None,:]\n",
" return out\n",
"\n",
"\n",
" def get_output(self,x):\n",
" y = np.zeros((1,x.shape[1]))\n",
" t_vals = np.arange(0, self.n_points-1, 0.01)\n",
" for t in t_vals:\n",
" i = np.floor(t)\n",
" prop = t-i\n",
" i = int(i)\n",
" y = y+ prop * self.intensity(x, self.means[:,[i]], self.stds[:,[i]], self.values[i])\n",
" y = y+ (1-prop) * self.intensity(x,self.means[:,[i+1]], self.stds[:,[i+1]], self.values[i+1])\n",
" y = np.clip(y,-10,10)\n",
" return y"
],
"metadata": {
"id": "rt4FS42dIa9_"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Code to draw 2D slide through the four dimensional function\n",
"# Again, you don't really need to read this.\n",
"def draw_2D_slice(model, dim1, dim2, first_other_dim_value = 0.5, second_other_dim_value = 0.6):\n",
"\n",
" #Create pretty colormap as in book\n",
" my_colormap_vals_hex =('2a0902', '2b0a03', '2c0b04', '2d0c05', '2e0c06', '2f0d07', '300d08', '310e09', '320f0a', '330f0b', '34100b', '35110c', '36110d', '37120e', '38120f', '39130f', '3a1410', '3b1411', '3c1511', '3d1612', '3e1613', '3f1713', '401714', '411814', '421915', '431915', '451a16', '461b16', '471b17', '481c17', '491d18', '4a1d18', '4b1e19', '4c1f19', '4d1f1a', '4e201b', '50211b', '51211c', '52221c', '53231d', '54231d', '55241e', '56251e', '57261f', '58261f', '592720', '5b2821', '5c2821', '5d2922', '5e2a22', '5f2b23', '602b23', '612c24', '622d25', '632e25', '652e26', '662f26', '673027', '683027', '693128', '6a3229', '6b3329', '6c342a', '6d342a', '6f352b', '70362c', '71372c', '72372d', '73382e', '74392e', '753a2f', '763a2f', '773b30', '783c31', '7a3d31', '7b3e32', '7c3e33', '7d3f33', '7e4034', '7f4134', '804235', '814236', '824336', '834437', '854538', '864638', '874739', '88473a', '89483a', '8a493b', '8b4a3c', '8c4b3c', '8d4c3d', '8e4c3e', '8f4d3f', '904e3f', '924f40', '935041', '945141', '955242', '965343', '975343', '985444', '995545', '9a5646', '9b5746', '9c5847', '9d5948', '9e5a49', '9f5a49', 'a05b4a', 'a15c4b', 'a35d4b', 'a45e4c', 'a55f4d', 'a6604e', 'a7614e', 'a8624f', 'a96350', 'aa6451', 'ab6552', 'ac6552', 'ad6653', 'ae6754', 'af6855', 'b06955', 'b16a56', 'b26b57', 'b36c58', 'b46d59', 'b56e59', 'b66f5a', 'b7705b', 'b8715c', 'b9725d', 'ba735d', 'bb745e', 'bc755f', 'bd7660', 'be7761', 'bf7862', 'c07962', 'c17a63', 'c27b64', 'c27c65', 'c37d66', 'c47e67', 'c57f68', 'c68068', 'c78169', 'c8826a', 'c9836b', 'ca846c', 'cb856d', 'cc866e', 'cd876f', 'ce886f', 'ce8970', 'cf8a71', 'd08b72', 'd18c73', 'd28d74', 'd38e75', 'd48f76', 'd59077', 'd59178', 'd69279', 'd7937a', 'd8957b', 'd9967b', 'da977c', 'da987d', 'db997e', 'dc9a7f', 'dd9b80', 'de9c81', 'de9d82', 'df9e83', 'e09f84', 'e1a185', 'e2a286', 'e2a387', 'e3a488', 'e4a589', 'e5a68a', 'e5a78b', 'e6a88c', 'e7aa8d', 'e7ab8e', 'e8ac8f', 'e9ad90', 'eaae91', 'eaaf92', 'ebb093', 'ecb295', 'ecb396', 'edb497', 'eeb598', 'eeb699', 'efb79a', 'efb99b', 'f0ba9c', 'f1bb9d', 'f1bc9e', 'f2bd9f', 'f2bfa1', 'f3c0a2', 'f3c1a3', 'f4c2a4', 'f5c3a5', 'f5c5a6', 'f6c6a7', 'f6c7a8', 'f7c8aa', 'f7c9ab', 'f8cbac', 'f8ccad', 'f8cdae', 'f9ceb0', 'f9d0b1', 'fad1b2', 'fad2b3', 'fbd3b4', 'fbd5b6', 'fbd6b7', 'fcd7b8', 'fcd8b9', 'fcdaba', 'fddbbc', 'fddcbd', 'fddebe', 'fddfbf', 'fee0c1', 'fee1c2', 'fee3c3', 'fee4c5', 'ffe5c6', 'ffe7c7', 'ffe8c9', 'ffe9ca', 'ffebcb', 'ffeccd', 'ffedce', 'ffefcf', 'fff0d1', 'fff2d2', 'fff3d3', 'fff4d5', 'fff6d6', 'fff7d8', 'fff8d9', 'fffada', 'fffbdc', 'fffcdd', 'fffedf', 'ffffe0')\n",
" my_colormap_vals_dec = np.array([int(element,base=16) for element in my_colormap_vals_hex])\n",
" r = np.floor(my_colormap_vals_dec/(256*256))\n",
" g = np.floor((my_colormap_vals_dec - r *256 *256)/256)\n",
" b = np.floor(my_colormap_vals_dec - r * 256 *256 - g * 256)\n",
" my_colormap_vals = np.vstack((r,g,b)).transpose()/255.0\n",
" my_colormap = ListedColormap(my_colormap_vals)\n",
"\n",
" x1_vals = np.arange(0.0, 1.0, 0.01)\n",
" x2_vals = np.arange(0.0, 1.0, 0.01)\n",
" x1_mesh, x2_mesh = np.meshgrid(x1_vals,x2_vals)\n",
" n_vals = x1_mesh.shape[0]\n",
"\n",
" x1 = np.reshape(x1_mesh,(1,n_vals*n_vals))\n",
" x2 = np.reshape(x2_mesh,(1,n_vals*n_vals))\n",
"\n",
" x = np.ones((4,n_vals*n_vals))\n",
" x[dim1,:] = x1\n",
" x[dim2,:] = x2\n",
" if((dim1==0 and dim2 ==1) or (dim1==1 and dim2 ==0)):\n",
" x[2,:] = x[2,:] * first_other_dim_value\n",
" x[3,:] = x[3,:] * second_other_dim_value\n",
" message = \"$x_{2}$ = %3.3f, $x_3$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
" if((dim1==0 and dim2 ==2) or (dim1==2 and dim2 ==0)):\n",
" x[1,:] = x[1,:] * first_other_dim_value\n",
" x[3,:] = x[3,:] * second_other_dim_value\n",
" message = \"$x_{1}$ = %3.3f, $x_3$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
" if((dim1==0 and dim2 ==3) or (dim1==3 and dim2 ==0)):\n",
" x[1,:] = x[1,:] * first_other_dim_value\n",
" x[2,:] = x[2,:] * second_other_dim_value\n",
" message = \"$x_{1}$ = %3.3f, $x_2$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
" if((dim1==1 and dim2 ==2) or (dim1==2 and dim2 ==1)):\n",
" x[0,:] = x[0,:] * first_other_dim_value\n",
" x[3,:] = x[3,:] * second_other_dim_value\n",
" message = \"$x_{0}$ = %3.3f, $x_3$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
" if((dim1==1 and dim2 ==3) or (dim1==3 and dim2 ==1)):\n",
" x[0,:] = x[0,:] * first_other_dim_value\n",
" x[2,:] = x[2,:] * second_other_dim_value\n",
" message = \"$x_{0}$ = %3.3f, $x_2$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
" if((dim1==2 and dim2 ==3) or (dim1==3 and dim2 ==2)):\n",
" x[0,:] = x[0,:] * first_other_dim_value\n",
" x[1,:] = x[1,:] * second_other_dim_value\n",
" message = \"$x_{0}$ = %3.3f, $x_1$=%3.3f\"%(first_other_dim_value, second_other_dim_value)\n",
"\n",
" y = model.get_output(x)\n",
" y[0,0] = -10; y[0,1]=10 # Hack the first two values so we see whole range of colormap\n",
" y_mesh = np.reshape(y,(n_vals, n_vals))\n",
"\n",
"\n",
" fig, ax = plt.subplots()\n",
" fig.set_size_inches(7,7)\n",
" pos = ax.contourf(x1_mesh, x2_mesh, y_mesh, levels=256 ,cmap = my_colormap, vmin=-10,vmax=10.0)\n",
" ax.set_xlabel('Dimension x%d'%dim1);ax.set_ylabel('Dimension x%d'%dim2)\n",
" ax.set_title(message)\n",
" levels = np.array([0])\n",
" ax.contour(x1_mesh, x2_mesh, y_mesh, levels, cmap=my_colormap)\n",
" cb = fig.colorbar(pos)\n",
" cb.set_ticks((-10,-5,0,5,10))\n",
" plt.show()"
],
"metadata": {
"id": "g0sosSU4RdU3"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Create an instance of our model"
],
"metadata": {
"id": "RlHjBpcyjcw4"
}
},
{
"cell_type": "code",
"source": [
"np.random.seed(3)\n",
"model = BlackBoxModel()"
],
"metadata": {
"id": "-JXgQD4oT3J1"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"The four inputs to the model might represent the four inputs measures of our debt, age, income etc, and the output represents the credit score.\n",
"\n",
"As a responsible model owner, we want to understand our model and make sure that it is doing something sensible. \n",
"\n",
"Unfortunately, the model describes a four dimensional function, which makes it really hard to understand (and imagine, that there could easily be hundreds of input in a real model).\n",
"\n",
"One thing that we can do it look at the effect of two of the inputs at one time. For example, we can look at how inputs 0 and 1 change when we fix dimension 2 to 0.2 and dimension 3 to 0.9. The black line represents the decision boundary (where the model predicts a credit score of zero). If we are on the wrong side of this boundary, then our loan is refused."
],
"metadata": {
"id": "6LxuB6p3k-VM"
}
},
{
"cell_type": "code",
"source": [
"draw_2D_slice(model,0,1,0.2,0.9)"
],
"metadata": {
"id": "NblKr3W0dBJJ"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Similarly, we could look at how inputs 1 and 3 change the input when we set input 0 to 0.3 and input 2 to 0.2:"
],
"metadata": {
"id": "Fp2GeFn5mIRW"
}
},
{
"cell_type": "code",
"source": [
"draw_2D_slice(model,1,3,0.3,0.2)"
],
"metadata": {
"id": "VzIe0py5d5Bk"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"This tells us something -- it might be good for a reality check if we had some expectations about what effect each input would have, but it's still hard to ensure that the model does something sensible everywhere, especially for models where there are thousands of inputs. Unfortunately, there are basically no good solutions to this problem at the time of writing."
],
"metadata": {
"id": "A9XUV9B6m7v0"
}
},
{
"cell_type": "markdown",
"source": [
"However, let's view this from the perspective of a customer. We can assume that the four inputs have some particular values, and see what the output is."
],
"metadata": {
"id": "1w968kJQjjUm"
}
},
{
"cell_type": "code",
"source": [
"x = np.array([[0.3],[0.8],[0.6],[0.3]])\n",
"y = model.get_output(x)\n",
"print(\"Your credit score is %3.3f\"%(y))\n",
"print(\"Sorry, your loan is refused\")"
],
"metadata": {
"id": "Nr71IahkjfV3"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Well, that is bad news. Why was our loan refused? We'd like to understand what we could do to improve our credit score. One way to do this is through individual conditional expectation or ICE plots ([Goldstein et al. 2015](https://arxiv.org/abs/1309.6392)). These take shows how the model output would change as we vary a single feature. Essentially, they answer the question: what if the $k^{th}$ feature had taken another value?"
],
"metadata": {
"id": "mafmi3dSkTuf"
}
},
{
"cell_type": "code",
"source": [
"def ice_plot(model, x, k):\n",
" # Get output for the input\n",
" y = model.get_output(x)\n",
"\n",
" # Possible values of the k'th dimension of the input\n",
" x_k_all = np.arange(0,1,0.001)\n",
" # TODO write code that varies the k'th dimension of x and runs the model on the result to create a series of outputs y\n",
" # Replace this line\n",
" y_all = np.zeros_like(x_k_all)\n",
"\n",
"\n",
"\n",
" fig, ax = plt.subplots()\n",
" ax.plot(x_k_all, np.squeeze(y_all), 'r-')\n",
" ax.plot(x[k],y,'ro') ;\n",
" ax.plot([0,1.0],[0.0,0.0],'k--')\n",
" ax.set_xlabel('Dimension x%d'%(k))\n",
" ax.set_ylabel('Credit score')\n",
" ax.set_xlim(0,1)\n",
" ax.set_ylim([-10,10])\n",
"\n",
" plt.show()\n",
"\n"
],
"metadata": {
"id": "v2nNsvW-m2fb"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": [
"ice_plot(model, x, 0)\n",
"ice_plot(model, x, 1)\n",
"ice_plot(model, x, 2)\n",
"ice_plot(model, x, 3)"
],
"metadata": {
"id": "5I91gzfSnL9N"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We can learn something from this. For example, decreasing the value of $x_{3}$ would be the most effective way to increase our credit score. However, this might be impossible if it is a variable we can't control like our age. Perhaps decreasing $x_0$ and $x_{1}$ might improve it further. Well... perhaps, but we don't know what is going on in our model; it might also make things much worse.\n",
"\n",
"Local interpretable model-agnostic explanations or LIME ([Ribeiro et al. 2016](https://arxiv.org/abs/1602.04938)) approximate the main machine learning model locally around a given input using a simpler model that is easier to understand. \n",
"\n",
"The principle is simple. First, we sample some points $\\mathbf{x}_{i}$ close to the input $\\mathbf{x}$ that we are interested in. Then we find the outputs $\\mathbf{y}_i$ that correspond to those inputs. Now we have a training set, and we can train any other kind of model that explains this small area of the input space. This can be a model that is much more interpretable and easier to understand such as a linear model or a tree."
],
"metadata": {
"id": "RmjuAR7HojtR"
}
},
{
"cell_type": "code",
"source": [
"# TODO -- Choose 100 points where each element of x is perturbed by noise sampled from a uniform distribution\n",
"# that takes values between [-0.05 and 0.05]. Then run these points through the model.\n",
"# Replace these lines\n",
"x_lime_train = np.matlib.repmat(x, 1, 100)\n",
"y_lime_train = np.ones((1,100))\n",
"\n",
"# BEGIN_ANSWER\n",
"x_lime_train = x_lime_train + np.random.uniform(low=-0.05,high=0.05,size=x_lime_train.shape)\n",
"y_lime_train = model.get_output(x_lime_train)\n",
"# END_ANSWER"
],
"metadata": {
"id": "-GprSftsnS0M"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We'll train a linear model:\n",
"\n",
"\\begin{equation}\n",
"y = \\beta_0 + \\boldsymbol\\phi^{T}\\mathbf{x}\n",
"\\end{equation}"
],
"metadata": {
"id": "yTFDYbqGqmcA"
}
},
{
"cell_type": "code",
"source": [
"# TODO -- train this model using a least squares loss\n",
"# to find values for the offset \\beta_0 and the four slopes in \\phi\n",
"# One way to do this is with sklearn.linear_model\n",
"# Replace this line\n",
"beta = 0; phi = np.zeros((1,4))\n",
"\n",
"print(phi)"
],
"metadata": {
"id": "5e4VPh40qlEl"
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"This model is easilly intepretable. The k'th coeffeicient tells us the how much (and in which direction) changing the value of the k'th input will change the output. This is only valid in the vicinity of the input $x$.\n",
"\n",
"Note that a more sophisticated version of LIME would weight the training points according to how close they are to the original data point of interest."
],
"metadata": {
"id": "hsZHWuVWtzIK"
}
}
]
}