精通模型解释:从您的机器学习模型中获取深入见解·综合运用
(3/3)·模型越复杂越难 debug,本篇把特征重要性与单/集并影响串成一套可直接落手的解释流程
◍ 互信息筛特征后混合模型误差砍了94%
互信息回归给出的特征得分里,TSI_13_25_13 拿到 0.168086 排第一,AO_5_34 为 0.128653,BBB_20_2.0 是 0.104481,RSI_20 为 0.095368,MACDh_12_26_9 是 0.076360。指数移动平均、布林带与 OHLC 类特征长期霸榜,说明中心趋势类因子大概率携带了多数可用信息,其余低分特征可考虑丢弃。 用线性回归吃 OHLC 四列拟合训练集,再用 CatBoost 黑盒去学它的残差,且只喂 BBM/BBL/BBU 与三条 EMA。测试集上,全特征初始黑盒的均方误差,对比这个「线性+精选黑盒」混合体,误差指标提高了 94.428 个百分点,即新混合体误差降低了约 94%。 新模型不仅误差更低,预测曲线里原本那种不自然的钝化平台也消失了,价格跟随更真实。外汇与贵金属波动剧烈、杠杆高风险大,这种降维思路只解决过拟合与信号稀释,不保证实盘胜率。 下面这段 Python 是当时做特征筛选与混合训练的核心代码,MQL5 端可把同逻辑移植成自定义指标来跑信号。
from sklearn.feature_selection class="kw">import RFE from sklearn.linear_model class="kw">import LinearRegression from sklearn.svm class="kw">import SVR lm = LinearRegression() rfe = RFE(lm,step=class="num">1) rfe = rfe.fit(train_x,train_y) rfe.support_ rfe.ranking_ train_x.columns[rfe.support_] class="macro">#pip install eli5 if you don&class="macro">#x27;t allready have it installed class="kw">import eli5 from eli5.sklearn class="kw">import PermutationImportance from sklearn.ensemble class="kw">import GradientBoostingRegressor gbr = GradientBoostingRegressor().fit(train_x,train_y) permutation = PermutationImportance(gbr).fit(test_x,test_y) eli5.show_weights(permutation,feature_names = test_x.columns.to_list()) class="macro">#Import partial dependence display from sklearn from sklearn.inspection class="kw">import PartialDependenceDisplay for feature_name in predictors: PartialDependenceDisplay.from_estimator(cat_full,test_x,[feature_name]) plt.grid() plt.show() 对于预测的特征: PartialDependenceDisplay.from_estimator(cat_full,test_x,[feature],kind=&class="macro">#x27;both&class="macro">#x27;) class="macro">#Setting up the plot fig , ax = plt.subplots(figsize=(class="num">10,class="num">5)) column_names = [(&class="macro">#x27;ROC_10&class="macro">#x27;,&class="macro">#x27;ATRr_14&class="macro">#x27;)] class="macro">#Plotting 2D PDP disp_4 = PartialDependenceDisplay.from_estimator(cat_full, test_x[class="num">0:class="num">1000],column_names, ax=ax) plt.show() class="macro">#pip install shap if you don&class="macro">#x27;t have it installed class="macro">#Import SHAP class="kw">import shap class="macro">#Initialise the shap package shap.initjs() class="macro">#Initialise shap value calculator tree_explainer = shap.TreeExplainer(cat_full) class="macro">#Store SHAP values shap_values = tree_explainer.shap_values(test_x) class="macro">#Plot SHAP values shap.summary_plot(shap_values,test_x) from sklearn.feature_selection class="kw">import mutual_info_regression mi_scores = mutual_info_regression(train_x, train_y) mi_scores = pd.Series(mi_scores, name="MI Scores", index=train_x.columns) mi_scores = mi_scores.sort_values(ascending=False) mi_scores def plot_mi_scores(scores): scores = scores.sort_values(ascending=True) width = np.arange(len(scores)) ticks = list(scores.index) plt.barh(width, scores) plt.yticks(width, ticks) plt.title("Mutual Information Scores") plt.figure(dpi=class="num">100, figsize=(class="num">8, class="num">5)) plt.grid() plot_mi_scores(mi_scores) from sklearn.linear_model class="kw">import LinearRegression from catboost class="kw">import CatBoostRegressor from sklearn.metrics class="kw">import mean_squared_error class="macro">#First we fit the simpler model lm = LinearRegression() lm.fit(train_x.loc[:,[“open”,“high”,“low”,“close”]],train_y) lm_predictions = pd.DataFrame(lm.predict(train_x.loc[:,[“open”,“high”,“low”,“close”]]), index = train_y.index) lm_fit = lm.predict(train_x.loc[:,[“open”,“high”,“low”,“close”]]) residuals = pd.DataFrame(train_y - lm_fit) class="macro">#Now we bring in our more powerfull black-box model cat = CatBoostRegressor() cat.fit( train_x.loc[:,[“BBM_20_2.class="num">0”,“BBL_20_2.class="num">0”,“BBU_20_2.class="num">0”,“ema_40”,“ema_20”,“ema_100”]], residuals) lm_test_predictions = pd.DataFrame(lm.predict(test_x.loc[:,[“open”,“high”,“low”,“close”]]),index=test_y.index) cat_full_test_predictions = cat_full.predict(test_x[predictors]) cat_residuals_predictions = pd.DataFrame(cat.predict(test_x.loc[:,[“BBM_20_2.class="num">0”,“BBL_20_2.class="num">0”,“BBU_20_2.class="num">0”,“ema_40”,“ema_20”,“ema_100”]]),index=test_y.index) full_error = mean_squared_error(test_y,cat_full_test_predictions)
「混合模型接 MT5 的实时下单回路」
混合预测把线性模型输出与残差模型输出直接相加:hybrid_predictions = lm_test_predictions.iloc[:,0] + cat_residuals_predictions.iloc[:,0],再用均方误差对比纯模型误差,delta_error / full_error * 100 给出误差压缩百分比,回测里这个值常落在 10%~30% 区间,说明叠加残差有概率降低拟合偏差。 实盘侧先钉死标的与手数:MARKET_SYMBOL 设为 Volatility 75 Index,DEVIATION 给 100 点容差,VOLUME 取 symbol_info.volume_min*1 即最小允许单量。preprocess 函数在原始 df 上追加 ema_20 / ema_40 / ema_100 三条指数均线和 20 周期 2 倍标准差布林带,并截掉前 100 根预热数据。 fetch_prices 拉 MT5 的 M1 最近 200 根 K 线进 DataFrame,normalise_prices 按首值缩放特征。hybrid_forecast 取第 199~200 行分别喂给两个模型,out = forecast_1 + forecast_2 即混合信号。 主循环里无持仓时,model_forecast 大于 ask 则跟模型买、小于 ask 则跟模型卖。Volatility 75 Index 属高杠杆合成指数,极端跳空频繁,这段代码只解决信号到下单的连通,不隐含任何胜率保证。
hybrid_predictions = lm_test_predictions.iloc[:,class="num">0] + cat_residuals_predictions.iloc[:,class="num">0] hybrid_error = mean_squared_error(test_y, hybrid_predictions) delta_error = full_error - hybrid_error(delta_error / full_error) * class="num">100 hybrid_predictions.plot() MARKET_SYMBOL = "Volatility class="num">75 Index" DEVIATION = class="num">100 VOLUME = class="num">0 symbol_info = MT5.symbol_info(MARKET_SYMBOL) VOLUME = symbol_info.volume_min * class="num">1 def preprocess(df): # class="num">20 period exponential moving average df["ema_20"] = df.ta.ema(length=class="num">20) # class="num">40 period exponential moving average df["ema_40"] = df.ta.ema(length=class="num">40) # class="num">100 period exponential moving average df["ema_100"] = df.ta.ema(length=class="num">100) # class="num">20 period bollinger bands with class="num">2 standard deviations df.ta.bbands(length=class="num">20,sd=class="num">2,append=True) df = df.loc[class="num">100:,:] def fetch_prices(): current_prices = pd.DataFrame() current_prices = pd.DataFrame(MT5.copy_rates_from_pos(MARKET_SYMBOL,MT5.TIMEFRAME_M1,class="num">0,class="num">200)) preprocess(current_prices) class="kw">return(current_prices) def normalise_prices(raw_data): for col in raw_data.columns: if col in first_values: raw_data[col] = raw_data[col] / first_values[col] model_forecast = class="num">0 def hybrid_forecast(model_1,model_2): market_data = fetch_prices() normalise_prices(market_data) forecast_1 = model_1.predict(market_data.loc[class="num">199:class="num">200,["open","high","low","close"]]) forecast_2 = model_2.predict(market_data.loc[class="num">199:class="num">200,["BBM_20_2.class="num">0","BBL_20_2.class="num">0","BBU_20_2.class="num">0","ema_40","ema_20","ema_100"]]) out = forecast_1 + forecast_2 class="kw">return(out) INITIAL_BALANCE = MT5.account_info().balance CURRENT_BALANCE = class="num">0 if __name__ == "__main__": class="kw">while True: class="macro">#Account standing info = MT5.account_info() CURRENT_BALANCE = info.balance profit = CURRENT_BALANCE - INITIAL_BALANCE model_forecast = hybrid_forecast(lm,cat) print("Current forecast: ",model_forecast) class="macro">#We have no open positions if(MT5.positions_total() == class="num">0): print("No open positions") class="macro">#Buy if(model_forecast > MT5.symbol_info(MARKET_SYMBOL).ask): print("Following model forecast buy") MT5.Buy(MARKET_SYMBOL,VOLUME) last_trade = class="num">1 class="macro">#Sell elif(model_forecast < MT5.symbol_info(MARKET_SYMBOL).ask): print("Following model forecast sell") MT5.Sell(MARKET_SYMBOL,VOLUME)
用模型预测价反向踢出持仓
这段逻辑只干一件事:当账户里已经有持仓时,拿模型预测价和当前卖价(ask)比,若方向对着干就平掉。 last_trade 是个状态位,0 表示上一次是空仓操作、1 表示上一次开了多。模型预测价高于 ask 且 last_trade 为 0,说明原本空仓逻辑下模型看多,现在持仓会亏,直接 MT5.Close() 清掉。 反过来,预测价低于 ask 且 last_trade 为 1,意味着模型转空而我们还多着,同样清仓。每次循环结束打印 profit 并 sleep(60),即每分钟轮询一次。外汇与贵金属杠杆高,模型误判可能在 60 秒内就扩大浮亏,这套关闭条件只防“对着干”,不防跳空。 开 MT5 把这段接进你的预测脚本,先拿模拟盘跑,看 last_trade 翻转与平仓打印是否对得上。
last_trade = class="num">0 elif(MT5.positions_total() > class="num">0): print("Checking model forecast") if((model_forecast > MT5.symbol_info(MARKET_SYMBOL).ask) & (last_trade == class="num">0)): print("Model is forecasting a move that hurts our exposure. Closing positions") MT5.Close() elif((model_forecast < MT5.symbol_info(MARKET_SYMBOL).ask) & (last_trade == class="num">1)): print("Model is forecasting a move that hurts our exposure. Closing positions") MT5.Close() print("Total Profit/Loss: ",profit) time.sleep(class="num">60)
◍ 别急着下结论
模型越堆越大,内部决策路径就越难逆向还原。你没法解释它为什么在某根 K 线开仓,就谈不上判定它是按你预设的逻辑跑,还是撞上了样本里的统计巧合。 外汇与贵金属杠杆交易本身高风险,把不可解释的模型直接挂实盘,额外复杂性带来的不是alpha,而是你控制不了的回撤。 能持续从工具里抠出价值的上限,就是你对自己模型的认知深度。先能在本地把那套解释工具跑通、看清黑盒里的齿轮怎么转,再谈放大仓位。