分歧问题:深入探讨人工智能的复杂性可解释性·进阶篇
(2/3)·当两种解释技术对你同一笔黄金EA模型给出相反结论,信任该交给谁?
◍ 用置换重要性与LIME拆解模型权重
训练完线性回归模型后,直接看系数容易误判,因为多重共线性会让权重符号失真。这里用 PermutationImportance 做置换检验:它打乱单个特征后看验证集均方误差的抬升幅度,抬升越多说明该特征越不可替代。 eli5 的 show_weights 会把每个预测因子的贡献排序列出,配合 sklearn 的 mean_squared_error 可量化打乱前后的误差差。若某因子置换后 MSE 从 0.012 跳到 0.031,那么它的边际信息量就明显高于置换后 MSE 仅到 0.014 的因子。 LIME 则从单个样本切入:用局部 surrogate 模型逼近原模型在该点附近的决策面。explainer.explain_instance 针对 scaled_data 第 test 行给出局部贡献表,能在 notebook 里直接看哪些特征把该笔预测往上推、哪些往下拉。外汇与贵金属波动受宏观脉冲影响,这类局部解释对识别过拟合样本有帮助,但高频行情下解释稳定性偏低,仅作概率参考。
epochs=class="num">100, verbose=class="num">0, callbacks=[early_stopping] ) class="kw">import eli5 from sklearn.metrics class="kw">import mean_squared_error from eli5.sklearn class="kw">import PermutationImportance from eli5.permutation_importance class="kw">import get_score_importances permutation = PermutationImportance(lm).fit(x_test,y_test) eli5.show_weights(permutation,feature_names = predictors) class="kw">import lime from lime class="kw">import lime_tabular explainer = lime_tabular.LimeTabularExplainer(training_data = np.array(x_train),mode=&class="macro">#x27;regression&class="macro">#x27;,feature_names=predictors) exp = explainer.explain_instance(data_row=scaled_data.iloc[test],predict_fn=lm.predict) exp.show_in_notebook(show_table=True)
「白盒与黑盒解释器的偏差陷阱」
知道模型底层结构时,白盒解释器能更忠实地还原行为,且计算效率高于黑盒。黑盒解释器(如 LIME、SHAP 的 Explainer 通用接口)与模型无关,但对模型形式做了简化假设,一旦假设被违反,解释就不可靠。 SHAP 黑盒图里每一行宽度由异常值决定,不反映特征重要性;要看特征在图中的垂直位置,越靠上越重要。点若蓝粉混在一起,往往暗示该特征与别的特征存在交互效应。 SHAP 算法假定特征独立,忽略数据集中交互作用,这是硬伤——违反该假设解释就会失真。图 6 黑盒与图 7 TreeExplainer 白盒对比中,最重要的 4 个特征若出现分歧,就是黑盒偏差的实际证据。 下面代码先跑黑盒 Explainer,再跑专为树模型设计的 TreeExplainer,两者 beeswarm 图可直接在 MT5 外接 Python 环境复现验证。外汇与贵金属模型解释同样受此偏差影响,高风险场景需优先用白盒。
class="kw">import shap explainer = shap.Explainer(xgb.predict,scaled_data.loc[test:test_end,predictors]) shap_values = explainer(scaled_data.loc[test:test_end,predictors]) shap.plots.beeswarm(shap_values) tree_explainer = shap.TreeExplainer(xgb) tree_shap_values = explainer(x_test) shap.plots.beeswarm(tree_shap_values)
特征重要性解释里的分歧到底长什么样
做模型解释时,最容易被忽略的坑是:不同解释器给同一模型算出的特征重要性并不一致。先把真正的分歧拆开看——特征排列顺序不同、前 n 个重要特征集合不一致、系数符号翻转、以及你关心的两个特征相对位置对调,这四类都算硬分歧,尤其当系数从正变负,基本说明模型内部贡献方向有争议,得回头查。 但不是所有数字不一样都叫分歧。不同算法算出的绝对重要性值本来就没法直接比,只要叙事结论一致(比如都说「认知度评级」最关键),就不算冲突。模型越复杂,估算越难,解释值有波动是预期内的,不是警报。 分歧的来源很具体。全局和局部解释不能直接放一起比;黑盒解释器为了通用性做了简化假设,假设不成立就会偏,和白盒对不上。更隐蔽的是特征输入顺序:SHAP 对顺序敏感,互信息不敏感。两个相同的 XGBRegressor,只改特征送入顺序,SHAP 下「身高」重要性突然跳升,前三虽不变但大多数特征上下乱跳。 eli5 的排列重要性实现也对顺序敏感,sklearn 的同技术却不敏感。实测两个相同 XGBRegressor 仅改输入顺序,eli5 下 kickPower_rating 系数从正变负,最重要特征都换了。若你用 bootstrap 或子集重采样,得知道这现象正常。 XGB 自带特征重要性评估函数可当参照。实测中认知度评级才是真最重要,但一小撮黑盒解释完全忽略前三,nearsPro 多次登顶其实不重要——别迷信「多次靠前就真重要」。黑盒和白盒在此模型上互相矛盾,但俩都错,纠结谁更忠实是浪费时间。玻璃盒树模型能认出真最重要特征,但非树模型未必能用;且它也曾误判 yearsPro 有信息性,说明解释器也可能一起认同错误信息。外汇与贵金属模型若接这类解释流程,高风险在于把估计当真相。
◍ 用 MT5 脚本抓数据再拿三个解释器互殴
把行情和指标导出来是第一步。下面这段 MQL5 脚本在 OnStart 里一口气挂了 MA(20)、RSI(60)、CCI(10)、AO、BBANDS(120) 和 ATR(14),请求 10000 根 K 线外加 50 根缓冲,把读数写进数组等后面塞 CSV。 拿到 CSV 后,用 InterpretML 做训练测试分割,分别跑了线性模型、XGB 和深度神经网络。深神经网用 alibi 的 ALE 解释,动量振荡指标(AO)的 ALE 曲线随数值增大斜率向下,说明它倾向提示价格下跌概率上升;而 RSI 的 ALE 接近水平线,基本没信息量。 换 SHAP 蜂群图,AO 又被标成最重要特征,但行宽受异常值影响不能当真。互信息排名里前四位是 BBANDS 120 的 MID/HIGH/LOW 和 MA 20(值 1.739 / 1.731 / 1.716 / 1.526),跟 SHAP 完全对不上。最后莫里斯敏感性分析说 MA 最重要、AO 次之,在这个样本里反而最接近真实重要性。 多个解释器同台是双刃剑:能防单一方法的偏见,但也必然制造分歧。外汇和贵金属市场高波动、高杠杆,这种特征重要性的矛盾没法靠拍脑袋消解,你对自己选哪个解释器的信心,本质上取决于数据集和模型,而不是某张图好看。
class=class="str">"cmt">//---Our handlers for our indicators class="type">int ma_handle; class="type">int rsi_handle; class="type">int cci_handle; class="type">int ao_handle; class="type">int bbands_handle; class="type">int atr_handle; class=class="str">"cmt">//---Data structures to store the readings from our indicators class="type">class="kw">double ma_reading[]; class="type">class="kw">double rsi_reading[]; class="type">class="kw">double cci_reading[]; class="type">class="kw">double ao_reading[]; class="type">class="kw">double bb_high_reading[]; class="type">class="kw">double bb_low_reading[]; class="type">class="kw">double bb_mid_reading[]; class="type">class="kw">double atr_reading[]; class=class="str">"cmt">//---File name class="type">class="kw">string file_name = "The Dissagrement Problem Data.csv"; class=class="str">"cmt">//---Amount of data requested class="type">int size = class="num">10000; class="type">int size_fetch = size + class="num">50; class="type">void OnStart() { class=class="str">"cmt">//---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_CURRENT,class="num">20,class="num">0,MODE_EMA,PRICE_CLOSE); rsi_handle = iRSI(_Symbol,PERIOD_CURRENT,class="num">60,PRICE_CLOSE); cci_handle = iCCI(_Symbol,PERIOD_CURRENT,class="num">10,PRICE_CLOSE); ao_handle = iAO(_Symbol,PERIOD_CURRENT); bbands_handle = iBands(_Symbol,PERIOD_CURRENT,class="num">120,class="num">0,class="num">0.2,PRICE_CLOSE); atr_handle = iATR(_Symbol,PERIOD_CURRENT,class="num">14); class=class="str">"cmt">//---Set the values as series CopyBuffer(ma_handle,class="num">0,class="num">0,size_fetch,ma_reading); ArraySetAsSeries(ma_reading,true); CopyBuffer(rsi_handle,class="num">0,class="num">0,size_fetch,rsi_reading); ArraySetAsSeries(rsi_reading,true); CopyBuffer(cci_handle,class="num">0,class="num">0,size_fetch,cci_reading); ArraySetAsSeries(cci_reading,true);
「把多指标快照落盘成 CSV」
抓完指标缓冲后,下一步是把 AO、布林带(120 周期)和 ATR(14 周期)一并写进文件,方便拿去给小布做离线特征工程。 先挨个 CopyBuffer 把句柄数据搬进数组,再用 ArraySetAsSeries(...,true) 把序列倒序,保证 ao_reading[0] 对应最新一根 K 线;布林带的三条线分别用缓冲区索引 0/1/2 取中轨、上轨、下轨。
| FileOpen 用 FILE_WRITE | FILE_ANSI | FILE_CSV 加逗号分隔打开目标文件,循环从 i=-1 到 size:首轮写表头,含 Time/Open/High/Low/Close 及 MA20、RSI60、CCI10、AO、BBANDS120 三轨、ATR14 共 14 列;之后每根 K 用 iTime/iOpen 等函数取 OHLC 再拼指标数组写行。 |
|---|
外汇与贵金属波动剧烈、杠杆高,这套导出仅用于历史复盘与概率统计,实盘信号仍要结合当下风控。
CopyBuffer(ao_handle,class="num">0,class="num">0,size_fetch,ao_reading); ArraySetAsSeries(ao_reading,true); CopyBuffer(bbands_handle,class="num">0,class="num">0,size_fetch,bb_mid_reading); ArraySetAsSeries(bb_mid_reading,true); CopyBuffer(bbands_handle,class="num">1,class="num">0,size_fetch,bb_high_reading); ArraySetAsSeries(bb_high_reading,true); CopyBuffer(bbands_handle,class="num">2,class="num">0,size_fetch,bb_low_reading); ArraySetAsSeries(bb_low_reading,true); CopyBuffer(atr_handle,class="num">0,class="num">0,size_fetch,atr_reading); ArraySetAsSeries(atr_reading,true); class=class="str">"cmt">//---Write to file class="type">int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(class="type">int i=-class="num">1;i<=size;i++){ if(i == -class="num">1){ FileWrite(file_handle,"Time","Open","High","Low","Close","MA class="num">20","RSI class="num">60","CCI class="num">10","AO","BBANDS class="num">120 MID","BBANDS class="num">120 HIGH","BBANDS class="num">120 LOW","ATR class="num">14"); } else{ FileWrite(file_handle,iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), ma_reading[i], rsi_reading[i], cci_reading[i], ao_reading[i], bb_mid_reading[i], bb_high_reading[i], bb_low_reading[i]