可解釋的AI:用LIME解釋撲克遊戲

可解釋的AI(XAI)一直是人們研究的一個方向,在這篇文章中,我們將看到如何使用LIME來解釋一個模型是如何學習撲克規則的。在這個過程中,我們將介紹:

如何將LIME應用到撲克遊戲中;

LIME如何工作;

LIME 的優點和缺點是什麼。

可解釋的AI:用LIME解釋撲克遊戲

將LIME應用到撲克遊戲中

目標

我們的目標是建立一個可以預測撲克牌的模型。“五張”使用一種撲克牌的遊戲規則,其中的組合決定了你是否贏得了這一輪的比賽。

可解釋的AI:用LIME解釋撲克遊戲

我們手裡有五張牌,這裡我們的目標是希望模型能預測手裡有哪一手牌。

資料集

我們的資料來自UCI機器學習庫

(https://archive。ics。uci。edu/ml/datasets/Poker+Hand)

。在資料集中,牌是透過從花色中分離出秩(卡片的值)進行編碼的。

可解釋的AI:用LIME解釋撲克遊戲

為了確保有足夠的資料來訓練模型,我們使用了一百萬行的資料集用於訓練,在下面的圖片中展示了一些例子:

模型

使用硬編碼決定你哪一手牌的規則是很容易的。從順子到四張,根據規則排序即可。但是當我們想要透過一個模型來學習規則時,就比較複雜了,但是如果我們成功的訓練好了這個模型,我們就可以將這種方法應用於任何撲克遊戲中,不需要管理分類的基本規則是什麼。

對於模型,我們選擇了一個隨機森林分類器。使用hyperopt對模型的超引數進行了調優。加權f1得分為0。75,可以合理預測給定5張牌作為輸入的撲克牌。在本文末尾會有完整的程式碼。

LIME

使用LIME來確定為什麼我們的模型會做出這樣的預測。哪些牌以及為什麼主導了這次預測結果?這就是可以利用LIME的地方。

LIME透過在原始模型之上訓練一個可解釋模型來工作。這樣,即使原始模型不能告訴你它為什麼預測結果,你也可以使用LIME來確定是什麼影響了它的決策。我們將使用這個邏輯來確定為什麼這個隨機森林分類器預測某些結果。

現在讓我們看看他是如何工作的:

上面的分類器預測我們的牌是”一對“。為什麼會這樣預測呢?看看LIME解釋:

LIME構建了一個視覺化的圖。在垂直軸上是特徵值:顯示手中的牌的數字和花色。在橫軸上是各種特徵值對分類的貢獻。這些貢獻值被縮放為相同的維度,並顯示一個特徵是有利於預測(綠色),還是不利於預測(紅色)。

我們的第一手牌是一對,你可能會認為兩個a的貢獻最大。但是LIME告訴我們情況並非如此。在上面的圖表中,LIME認為第3張牌對分類的貢獻最大(儘管是負貢獻)。如果不使用可解釋的AI,我們根本沒法想到這是為什麼。研究為什麼這個確切的特徵觸發了LIME模型是做進一步探索性資料分析的一個極好的切入點。

我們再繼續研究另外一套:

使用LIME解釋

可以看到牌的數字比花色對同花順的分類貢獻更大。對於我們的理解這簡直是不可能的,因為同花順就是要有相同的花色。但是透過使用LIME,我們可以看到實際上是卡片數字被賦予了分類更多的權重。如果不使用可解釋的AI,我們很容易忽略這一點,但透過使用LIME,我們可以確保自己的假設得到驗證。

LIME幫助解釋為什麼模型會做出這樣的預測。無論使用它來確認模型是否觸發了我們所期望的功能,還是作為探索性分析的一部分,LIME都是都是一個強大的方法。

透過上面的兩個例子,我們可以看到LIME透過在原始模型之上訓練一個可解釋模型來工作。即使原始模型不能告訴你它為什麼預測結果,你也可以使用LIME來確定是什麼影響了它的決策。

LIME是如何工作的

為什麼要使用黑盒模型呢?就模型效能而言,黑盒模型通常比白盒模型具有優勢。但是它們的缺點就是可解釋性較低。2016年引入了LIME作為解決黑箱模型不透明問題的方法。為了理解LIME在後臺做了什麼,讓我們來看看LIME是如何工作的:

上圖解釋了LIME的概念,在使用LIME時需要考慮以下因素。

優點:

LIME可以在廣泛的資料集上很好地工作;

LIME比數學上更完整的方法(如SHAP值)要快得多;

解釋特定結果的LIME方法不會改變,即使底層黑盒模型改變了。

缺點:

LIME模型不能保證揭示所有的潛在決策;

LIME模型只能區域性應用,而不能全域性應用。

本文程式碼

最後就是本文的程式碼了:

from ctypes import alignment from functools import partial import matplotlib。pyplot as plt import numpy as np import pandas as pd from hyperopt import STATUS_OK, Trials, fmin, hp, space_eval, tpe from hyperopt。pyll import scope from lime import lime_tabular from sklearn。ensemble import RandomForestClassifier from sklearn。model_selection import train_test_split from sklearn。metrics import f1_score def objective(params:dict, X_train:pd。DataFrame, y_train:pd。DataFrame, X_val:pd。DataFrame, y_val:pd。DataFrame)->dict: “”“This function is used as objecive for the hyperparameter tuning Parameters —————— params : dict parameters for the model X_train : pd。Dataframe Feature dataset for training y_train : pd。DataFrame Target variable for training X_val : pd。DataFrame Feature dataset for validation y_val : pd。DataFrame Target variable for validation Returns ————- dict loss and status for hyperopt ”“” # define the model model = RandomForestClassifier(random_state=1, **params) # train the model model。fit(X_train,y_train) # validate and get the score score = model。score(X_val, y_val) return {“loss”: -score, “status”: STATUS_OK} def find_best_parameters(seed:int=2, **kwargs)->dict: “”“In this function hpo is performed Parameters —————— seed : int, optional random seed, by default 2 Returns ————- dict best paramers found by hyperopt ”“” # initialize trials trial = Trials() # initialize the objetve function partial_objective = partial( objective, X_train=kwargs[‘X_train’], y_train=kwargs[‘y_train’], X_val=kwargs[‘X_val’], y_val=kwargs[‘y_val’] ) # initialize the search space for hyperopt params = {‘n_estimators’: scope。int(hp。quniform(‘n_estimators’, 100, 500, 10)), ‘max_depth’: scope。int(hp。quniform(‘max_depth’, 5, 60, 2)), ‘min_samples_leaf’: scope。int(hp。quniform(‘min_samples_leaf’, 1, 10, 1)), ‘min_samples_split’: scope。int(hp。quniform(‘min_samples_split’, 2, 10, 1))} # find best params best_argmin = fmin( fn=partial_objective, space=params, algo=tpe。suggest, max_evals=50, trials=trial, rstate=np。random。default_rng(seed), ) best_params = space_eval(params, best_argmin) return best_params # Tweak the output to make it look nicer def as_pyplot_figure( exp, classif, classes_names, instance_to_explain, label:int=1, figsize=(4, 4) ): “”“This function has been taked from the lime package and tweaked for this particular use case Parameters —————— exp : _type_ lime explanation of the instance to explain classif : _type_ clssification type classes_names : _type_ names of the classrs instance_to_explain : _type_ the instance of the data which should be explained label : int, optional label for protting - of the explanation instance, by default 1 figsize : tuple, optional desired size of pyplot in tuple format, defaults to (4,4)。 Returns ————- _type_ figure with the explanations ”“” # find the explanation for a particular label exp_list = exp。as_list(label=label) fig, ax = plt。subplots(figsize=figsize) vals = [x[1] for x in exp_list] names = [x[0] for x in exp_list] # plot the contributions vals。reverse() names。reverse() colors = [“green” if x > 0 else “red” for x in vals] pos = np。arange(len(exp_list)) + 0。5 ax。barh(pos, vals, align=“center”, color=colors) ax。set_yticks(pos, labels=names) limit = max(abs(min(vals)), abs(max(vals))) ax。set_xlim(left=-limit, right=limit) ax。set_xticks([]) ax。set_xlabel(“Contribution”) # Add second axis with the values of the cards suits = {1: “\u2661”, 2: “\u2660”, 3: “\u2662”, 4: “\u2663”} ranks = { 1: “Ace”, 2: “Two”, 3: “Three”, 4: “Four”, 5: “Five”, 6: “Six”, 7: “Seven”, 8: “Eight”, 9: “Nine”, 10: “Ten”, 11: “Jack”, 12: “Queen”, 13: “King”, } # etract the data from the explanation list_figures = [] for i in exp_list: if “S” in i[0]: if ‘=’ in i[0]: # logic for categorical new_string = i[0][i[0]。index(“S”) :] extract = int(new_string[ new_string。index(“=”)+1:]) list_figures。append(suits[extract]) else: # logic for continuous variables new_string = i[0][i[0]。index(“S”) :] extract = new_string[: new_string。index(“ ”)] list_figures。append(suits[instance_to_explain。loc[extract]]) elif “R” in i[0]: if ‘=’ in i[0]: # logic for categorical new_string = i[0][i[0]。index(“R”) :] extract = int(new_string[ new_string。index(“=”)+1:]) list_figures。append(ranks[extract]) else: # logic for continous variables new_string = i[0][i[0]。index(“R”) :] extract = new_string[: new_string。index(“ ”)] list_figures。append(ranks[instance_to_explain。loc[extract]]) # create second axis ax2 = ax。twinx() ax2。set_yticks(ticks=np。arange(len(exp_list)) + 0。5, labels=list_figures[::-1]) ax2。barh(pos, vals, align=“center”, color=colors) # add title if classif == “classification”: title = f“Why {classes_names[label][4:]}?” else: title = “Local explanation” plt。title(title) plt。tight_layout() return fig # Read dataset df_test = pd。read_csv(“。/data/df_test。csv”) df_train = pd。read_csv(“。/data/df_train。csv”) # Let‘s take the suit and the rank (value) of each card col_names = [“S1”, “R1”, “S2”, “R2”, “S3”, “R3”, “S4”, “R4”, “S5”, “R5”, “y”] df_train。columns = col_names df_test。columns = col_names # Define our hand combinations target_labels = [ “0 - High card”, “1 - One pair”, “2 - Two pairs”, “3 - Three of a kind”, “4 - Straight”, “5 - Flush”, “6 - Full house”, “7 - Four of a kind”, “8 - Straight flush”, “9 - Royal flush”, ] # get the training and validation sets y = df_train[“y”] X = df_train。drop(columns=“y”) X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0。3, random_state=1 ) # find best parameters best = find_best_parameters(X_train=X_train, X_val=X_val, y_train=y_train, y_val=y_val) # Get test data y_test = df_test[“y”] X_test = df_test。drop(columns=“y”) # Get train data y_train = df_train[“y”] X_train = df_train。drop(columns=“y”) # Fit with a black-box model on full train dataset model = RandomForestClassifier(random_state=42, **best) model。fit(X_train, y_train) # get the F1-score of the model on the test set y_pred = model。predict(X_test) f1score = f1_score(y_test, y_pred, average=’weighted‘) # define instances to explain (any instance from train / test can be taken here) instance_1 = pd。Series({’S1‘: 2, ’R1‘: 2, ’S2‘: 4, ’R2‘: 3, ’S3‘: 4, ’R3‘: 7, ’S4‘: 4, ’R4‘: 1, ’S5‘: 2, ’R5‘: 1}) instance_2 = pd。Series({’S1‘: 4, ’R1‘: 2, ’S2‘: 4, ’R2‘: 3, ’S3‘: 4, ’R3‘: 4, ’S4‘: 4, ’R4‘: 5, ’S5‘: 4, ’R5‘: 10}) # initialise LIME explainer = lime_tabular。LimeTabularExplainer( training_data=np。array(X_train), feature_names=X_train。columns, class_names=target_labels, mode=“classification”, categorical_features= [i for i in range(10)] ) for instance_to_explain, label in zip([instance_1, instance_2], [1, 5]): # create explanation exp = explainer。explain_instance( data_row=instance_to_explain, predict_fn=model。predict_proba, num_features=10, labels=[label] ) # visualize: using lime show_in_noteboook() exp。show_in_notebook(show_table=True) # visualize using the custom visualization as_pyplot_figure(exp=exp, classif=“classification”, classes_names=target_labels, instance_to_explain=instance_to_explain, label=label);

如果你需要你也可以在這裡找到它:

https://gist。github。com/O-Konstantinova/153284d4ec81e5ca6c9b049277117434#file-lime-poker-py