全文共 7141 字,预计学习时长 21 分钟
图源:Unsplash
从“约翰•纳什”到“板球世界杯”
还记得《美丽心灵》的男主(也是现实生活中传奇的一代数学家和经济学家)约翰•纳什吗?饱受精神分裂的酷刑,在他处于梦境一般的精神状态时,他的名字开始出现在70年代和80年代的经济学课本、进化生物学论文、政治学专着和数学期刊的各领域中。他的名字已经成为经济学或数学的一个名词。
如“纳什均衡”、“纳什谈判解”、“纳什程序”、“德乔治-纳什结果”、“纳什嵌入”和“纳什破裂”等。
与妻子一起数十年不断与精神梦魇的抗争,最终使他从疯癫中苏醒,与另外两位数学家在非合作博弈的均衡分析理论方面做出了开创性的贡献,对博弈论和经济学产生了重大影响。
成为一代传奇数学家和经济学家。
时光回到2007年,那是第一次举办T20板球世界杯的时候。
全世界都在讨论这个话题,但板球协会却十分谨慎——广告插播的时间从99秒缩减到了39秒。天哪!收入也减少了一大截。
然而长远来看,这个决定是最为明智的,2007年的广告插播收入是目前为止板球史上收入最高的。
2020年,世界的目光又将聚焦在印度板球团队,队长Virat Kohli将再次因为做关键决策而受到关注,其压力之大,可想而知。
图源:Unsplash
等等——这和机器学习可解释性,夏普利值(Shapley value),博弈论有什幺关系?
我们一起来看看:
有没有可能建立发明一种能以超高准确率支持所有关键决定的理论?如果能的话,那就太棒了。它能解决所有疑惑,比如说——第一个击球的击球手,选中的投球手,替补选手等等。
博弈论可以实现这个想法! 下文笔者将探索解读机器学习模型的另一种替代方法,由博弈论衍生而来。笔者还将介绍夏普利值在机器学习可解释性中的运用。
本文对博弈论零基础者完全友好,笔者将深入浅出地解释最基本的概念,着重介绍夏普利值,以及它是如何通过运用Python中的SHAP库解读机器学习模型的。
概 述
• 如何运用博弈论中的夏普利值来提高机器学习可解释性
• 全新视角解释黑盒子机器学习模型
目录
· 博弈论是什幺?
· 合作型博弈论
· 夏普利值:直觉
· 夏普利值在机器学习解释中的应用
· 在Python中使用SHAP的模型解读
· 使用夏普利模型进行全局解读
博弈论是什幺?
在进入本文核心之前,首先要搞清楚博弈论的定义,为之后将博弈论用于处理机器学习模型打好基础。
博弈论是指研究多个个体或团队之间在特定条件制约下的对局中利用相关方的策略,而实施对应策略的理论框架。它是研究如何在战略环境下,各个独立又互相竞争的主体如何做出最优决定的。
数学家冯∙诺伊曼、约翰∙纳什,经济学家奥斯卡·摩根斯特恩都是博弈论的专家。
看到这,读者可能会问——什幺是博弈?是国际象棋还是电子游戏?
图源:Unsplash
“博弈”是指有多个个体,每个个体都想将自己的结果最大化的情况。优化决策的过程往往依赖于其他个体的决定。博弈通常包括个体身份,偏好,可用策略,以及这些策略是如何影响结果的。
博弈论将这些情况通过数学概念表示出来,再决定如果每个玩家都理性行事,最终结果会是什幺样的。
· 或许会达到某种平衡(在同一个国家,人们都靠道路的同一侧行驶)
· 这种平衡可能对所有人都不利(人们乱扔垃圾,污染公共资源)
· 所有人都试着使自己的行为变得不可预测(战争中的军队部署)
从本质上来讲,博弈论是用数学模型来解读复杂人类行为的方式,先试着去了解它,再进行预测。
合作型博弈
合作型博弈论假设被称为“联盟”的参与玩家是决策过程中的主要单元,他们可以强制进行合作。
相应地,合作型博弈可以看作是玩家联盟间的竞争,而不是单个玩家间的竞争。
通过夏普利值的概念,我们试着去理解一下合作型博弈论。
图源:Unsplash
夏普利值背后的直觉
下文会使用很多图像和例子来解释新的概念。
首先设计一局合作型博弈。三个好朋友——Ram, Abhiraj和Pranav,一起出去吃饭。他们点了薯条、红酒和派一起分享。因为每个人吃的份量不是完全平均的,所以很难判定每个人应该付多少。有如下信息:
1. 如果Ram是自己一个人吃,他通常会花800
2. 如果是Abhiraj自己一个人吃,他通常会花560
3. 如果是Pranav自己一个人吃,他通常会花700
4. 如果Ram和Abhiraj两个人吃,各吃各的,两个人会付800
5. 如果Ram和Pranav两个人吃,各吃各的,两个人会付850
6. 如果Abhiraj和Pranav两个人吃,各吃各的,两个人会付720
7. 如果Ram,Abhiraj和Pranav三个人一起吃,他们会付900
看来三个人一起吃,需要付的总价是900。现在需要知道每个人应该付多少。
使用的方法如下:将3个人所有的排列组合方式按照顺序列起来,再看每个人需要付的钱增加了多少。
在这,顺序是Ram, Abhiraj, Pranav轮流来。如上文所述,Ram一个人应该付800。现在Ram和Abhiraj两个人一起也只付800所以Abhiraj不需要付额外的钱,也就是0。最后,三个人一起吃要付900,所以Pranav需要再付100。
将此方法轮流分析3个人,会得到如下边际支出值:
(Ram, Abhiraj, Pranav) – (800,0,100)
(Abhiraj, Ram, Pranav) – (560, 240, 100)
(Abhiraj, Pranav, Ram) – (560, 160, 180)
(Pranav, Ram, Abhiraj) – (700, 150, 50)
(Pranav, Abhiraj, Ram) – (700, 20, 180)
(Ram, Pranav, Abhiraj) – (800, 50, 50)
那幺,Ram, Abhiraj和Pranav每个人的夏普利值是多少呢?就是每个人的边际支出值的平均值!
比如说,Ram的夏普利值就是(800 + 240 + 180 + 150 + 180 + 800)/6 = 392,同样的,Abhiraj是207, Pranav是303。总价加起来是900.
现在知道了3个人一起出去要付的钱,在下文,笔者将探索夏普利值是如何运用到机器学习模型的。
图源:Unsplash
夏普利值在解读机器学习中的应用
首先应该对夏普利值有直观上的了解——想想怎幺能使用它来帮助解释黒盒机器学习模型。
给定一个例子,其中每个独立变量或者特征的值都是合作博弈的一部分。通常会假定预测值就是真实结果。为理解这一概念,再看另一个例子。
假设如下情形:
训练一个预测德里房价的机器学习模型。模型对某种房型的预测结果是51,00,000印度卢比,要解释这个预测结果是怎幺来的。这套公寓占地50码,有一个私人泳池和一个车库。
模型预测整套房子的均价是50,00,000卢比。和平均预测值相比,每个特征值占比多少?
从博弈论角度来讲,这里的“博弈”就是针对数据集中单个实例的预测。每个玩家就是实例的特征值,在游戏中相互合作(预测出结果),和上述三个好朋友一起吃饭的例子类似。
在买房例子中,特征值有如下三个,带泳池has_pool,带车库has_garage和占地50码area-50 ,这三个特征一起会得出51,00,000印度卢比的结果。我们的目标是要解释真实预测结果(51,00,000卢比)和平均预测结果(50,00,000)的区别,也就是1,00,000卢比的区别。
带泳池has_pool可能会多出30,000卢比,带车库会多出50,000卢比,占地50码会多收20,000卢比。加起来就是1,00,000——最终预测的值减去平均预测房价。
总结一下,每个变量的夏普利值也就是要找到每个部分对应的权重,这个值越准确越好,这样所有夏普利值也就等于预测值和平均值的差。也就是说,每个特征对预测值和期望值产生的偏差所做出的贡献和夏普利值相差无几。
现在知道了夏普利值背后的意义,也知道了夏普利值解读机器学习模型的用处,来看看夏普利值在Python中的应用吧。
在Python中使用夏普 (SHAP)解读模型
Python中的SHAP库有内嵌方程,可以使用夏普利值来解读机器学习模型。对于基于树的模型,和解读预测结果已知的黑盒模型的模型不可知解释器函数,它都有优化函数。
在模型不可知解释器中,SHAP能按照以下规则得出夏普利值。要得到特征X{i}的重要性:
1. 获取特征S中不含X{i}的子集
2. 将X{i}加到所有子集后预测结果,计算其影响
3. 合计所有结果,计算出特征的边缘值
对于子集,SHAP不会进行更多的操作,再重新训练每个子集。相反,对于被移除或者被剩下的特征,它只会用平均值进行替换,再生成预测结果。
现在来试试真实的数据集吧! 像前文描述的那样,笔者将使用Datahack平台上的大超市销售问题为例。
图源:Unsplash
主要目标在于预测不同商场出售的不同商品。可以从上述链接下载数据集。运用夏普利值,再使用一些可视化工具来探索本地和全局解释方法。
本文重点在于使用夏普利值对模型进行解释。
可以在终端安装SHAP库:
conda install -c conda-forge shap
现在,通过建模来预测销售情况。首先,导入必要的库:
<span><span># importing the required libraries</span></span>
<span><span>import</span> pandas <span>as</span> pd</span>
<span><span>import</span> numpy <span>as</span> np</span>
<span><span>import</span> shap</span>
<span><span>from</span> sklearn.model_selection <span>import</span> train_test_split</span>
<span><span>from</span> sklearn.metrics <span>import</span> mean_squared_error</span>
<span> </span>
<span><span>from</span> sklearn.linear_model <span>import</span> LinearRegression</span>
<span><span>from</span> sklearn.tree <span>import</span> DecisionTreeRegressor</span>
<span><span>from</span> sklearn.ensemble <span>import</span> RandomForestRegressor</span>
<span><span>from</span> xgboost.sklearn <span>import</span> XGBRegressor</span>
<span><span>from</span> sklearn.preprocessing <span>import</span> OneHotEncoder, LabelEncoder</span>
<span><span>from</span> sklearn <span>import</span> tree</span>
<span> </span>
<span><span>import</span> matplotlib.pyplot <span>as</span> plt</span>
<span>%matplotlib inline</span>
<span><span>import</span> warnings</span>
<span>warnings.filterwarnings(<span>’ignore'</span>)</span>
读取数据
<span><span># reading the data</span></span>
<span><span>df</span> = pd.read_csv(<span>’data.csv'</span>)</span>
处理缺失值
<span><span># imputing missing values in Item_Weight by median and Outlet_Size with mode</span></span>
<span>df[<span>’Item_Weight'</span>].fillna(df[<span>’Item_Weight'</span>].median(), inplace=<span>True</span>)</span>
<span>df[<span>’Outlet_Size'</span>].fillna(df[<span>’Outlet_Size'</span>].mode()[<span>0</span>], inplace=<span>True</span>)</span>
特征工程
<span><span># creating a broad category of type of Items</span></span>
<span>df[<span>’Item_Type_Combined'</span>] = df[<span>’Item_Identifier'</span>].apply(<span>lambda</span> df: df[<span>0</span>:<span>2</span>])</span>
<span>df[<span>’Item_Type_Combined'</span>] = df[<span>’Item_Type_Combined'</span>].map({<span>’FD'</span>:<span>’Food'</span>, <span>’NC'</span>:<span>’Non-Consumable'</span>, <span>’DR'</span>:<span>’Drinks'</span>})</span>
<span> </span>
<span>df[<span>’Item_Type_Combined'</span>].value_counts()</span>
<span> </span>
<span><span># operating years of the store</span></span>
<span>df[<span>’Outlet_Years'</span>] = <span>2013</span> – df[<span>’Outlet_Establishment_Year'</span>]</span>
<span> </span>
<span><span># modifying categories of Item_Fat_Content</span></span>
<span>df[<span>’Item_Fat_Content'</span>] = df[<span>’Item_Fat_Content'</span>].replace({<span>’LF'</span>:<span>’Low Fat'</span>, <span>’reg'</span>:<span>’Regular'</span>, <span>’low fat'</span>:<span>’Low Fat'</span>})</span>
<span>df[<span>’Item_Fat_Content'</span>].value_counts()</span>
数据预处理
<span><span># label encoding the ordinal variables</span></span>
<span>le = LabelEncoder()</span>
<span>df[<span>’Outlet'</span>] = le.fit_transform(df[<span>’Outlet_Identifier'</span>])</span>
<span>var_mod = [<span>’Item_Fat_Content'</span>,<span>’Outlet_Location_Type'</span>,<span>’Outlet_Size'</span>,<span>’Item_Type_Combined'</span>,<span>’Outlet_Type'</span>,<span>’Outlet'</span>]</span>
<span>le = LabelEncoder()</span>
<span><span>for</span> i <span>in</span> var_mod:</span>
<span>df[i] = le.fit_transform(df[i])</span>
<span> </span>
<span><span># one hot encoding the remaining categorical variables</span></span>
<span>df = pd.get_dummies(df, columns=[<span>’Item_Fat_Content'</span>,<span>’Outlet_Location_Type'</span>,<span>’Outlet_Size'</span>,<span>’Outlet_Type'</span>,</span>
<span> <span>’Item_Type_Combined'</span>,<span>’Outlet'</span>])</span>
训练测试分离
<span><span>#</span><span> dropping the ID variables and variables that have been used to extract new variables</span></span>
<span>df.drop([‘Item_Type’,’Outlet_Establishment_Year’, ‘Item_Identifier’, ‘Outlet_Identifier’],axis=1,inplace=True)</span>
<span><br /></span>
<span><span>#</span><span> separating the dependent and independent variables</span></span>
<span>X = df.drop(‘Item_Outlet_Sales’,1)</span>
<span>y = df[‘Item_Outlet_Sales’]</span>
<span><br /></span>
<span><span>#</span><span> creating the training and validation <span>set</span></span></span>
<span>X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.25, random_state=42)</span>
Shap初始化
<span><span># Need to load JS vis in the notebook</span></span>
<span><span>shap.initjs()</span></span>
XGBoost拟合
<span><span>xgb_model = XGBRegressor(n_estimators=1000, max_depth=10, learning_rate=0.001, random_state=0)</span></span>
<span><span>xgb_model.fit(X_train, y_train)</span></span>
生成预测结果
<span><span>y_predict = xgb_model.predict(X_test)</span></span>
性能评估
<span><span>mean_squared_error(y_test, y_predict)**(0.5)</span></span>
使用SHAP进行本地解释(如需预测id号是4776)
<span><span>explainer = shap.TreeExplainer(xgb_model)</span></span>
<span><span>shap_values = explainer.shap_values(X_train)</span></span>
<span><span>i = 4776</span></span>
<span><span>shap.force_plot(explainer.expected_value, shap_values[i], features=X_train.loc[4776], feature_names=X_train.columns)</span></span>
蓝色部分Shap为负值,显示了导致销售值朝着不好的方向前进的所有因素。红色部分则显示了相反因素。注意,这只是观察样本4776号。
接着,来看看一个能帮助生成总结的函数。
使用夏普利值进行全局解释
现在可以在每次观察的时候计算出每个特征的夏普利值,从整体的角度来看,可以使用夏普利值得到全局解释。看看这是如何实现的:
<span><span>shap.summary_plot(shap_values, features=X_train, feature_names=X_train.columns)</span></span>
把所有东西放在一个文件夹下,就能得到上述图像。它显示了x轴上的Shap值。在图上,左边所有的值都表示导致预测值向负方向移动的原因,右边的值表示导致预测值向正方向移动的原因。这些特征反应在左边的y轴上。
这里,右边展示的高物料需求值(MRP)主要是因为它们对销售值有积极贡献。相似地,对销售渠道为0的商品,会对商品销售起消极影响。
结语
随着更复杂的模型投入使用,可解释性仍然是机器学习和数据科学的一个非常重要的方面。
LIME和Shapley就是这样的两种方法,它们已经开始在行业中被采用。 随着深度学习的出现,有越来越多的研究关注如何解读自然语言处理(NLP)和计算机视觉模型。计算机视觉的可解释性已经在一定程度上得到了解决。
相信在不远的未来,会有更多进展。
图源:Unsplash
Be First to Comment