直推和主动机器学习中的梯度提升·进阶篇
◍ 少量标签也能逼近全量训练精度
做主动学习实验时,我先用批量查询策略跑了一遍,新数据上表现一般;但把生成的数据集喂给 GMM 后,开始出现有意思的信号。核心思路是:从带标签集里随机抽一部分做预训练,剩下的当查询池,用 AdaBoost(和 CatBoost 思路接近)当基分类器迭代补标。 半监督学习的结果天然带随机性,调了一轮学习者参数后,精度勉强追平了上一篇文章里的被动学习基线。理想状态下,只用少量标签的主动学习者,分类精度应当超过拿全部数据硬标的同类模型。 实测中,全量标签分类器准确率反而低于只训了 2000 个实例的主动学习者——这种反差在外汇与贵金属这种高噪声、高杠杆市场里尤其值得警惕,小样本抓到的边际信息可能比堆量更有效,但也可能只是过拟合幻象。 委员会查询那边,我同样用批处理策略避免逐条重训,建了 5 个 AdaBoost 组成的委员会(再多我觉得没意义,你可以自己试)。结果委员会成绩没跑赢被动委员会,原因说不清,可能纯随机。下面这段 Python 风格函数(可类比 MQL5 里用 OnTester 收集样本后送 GMM)展示了主动学习主循环,输出里被动学习 5483 条全量训练准确率仅 0.599,而 1000 标 + 1000 查的主动轮次在 0.565~0.585 间波动。 开 MT5 自己复现的话,重点看 query 轮次精度是否稳定在 0.58 上方;若掉到 0.56 以下,说明这批未标记池噪声太大,该换特征而非加模型。
def active_learner(data, labeled_size, unlabeled_size, batch_size, max_depth): X_raw = data[data.columns[class="num">1:-class="num">1]].to_numpy() y_raw = data[data.columns[-class="num">1]].to_numpy() # Isolate our examples for our labeled dataset. training_indices = np.random.randint(low=class="num">0, high=X_raw.shape[class="num">0] + class="num">1, size=labeled_size) X_train = X_raw[training_indices] y_train = y_raw[training_indices] # fit the model on all data cl = AdaBoostClassifier(DecisionTreeClassifier(max_depth=max_depth), n_estimators=class="num">50, learning_rate = class="num">0.01) cl.fit(X_raw, y_raw) print(&class="macro">#x27;Score for the passive learning: &class="macro">#x27;, cl.score(X_raw, y_raw), &class="macro">#x27; with train size: &class="macro">#x27;, data.shape[class="num">0]) # Isolate the non-training examples we&class="macro">#x27;ll be querying. X_pool = np.class="kw">delete(X_raw, training_indices, axis=class="num">0) y_pool = np.class="kw">delete(y_raw, training_indices, axis=class="num">0) # Pre-set our batch sampling to retrieve class="num">3 samples at a time. preset_batch = partial(uncertainty_batch_sampling, n_instances=batch_size) # Specify our core estimator along with its active learning model. cl = AdaBoostClassifier(DecisionTreeClassifier(max_depth=class="num">3), n_estimators=class="num">50, learning_rate = class="num">0.03) learner = ActiveLearner(estimator=cl, query_strategy=preset_batch, X_training=X_train, y_training=y_train) # Allow our model to query our unlabeled dataset for the most # informative points according to our query strategy(uncertainty sampling). N_QUERIES = unlabeled_size class=class="str">"cmt">// batch_size for index in range(N_QUERIES): query_index, query_instance = learner.query(X_pool) # Teach our ActiveLearner model the record it has requested. X, y = X_pool[query_index], y_pool[query_index] learner.teach(X=X, y=y) # Remove the queried instance from the unlabeled pool. X_pool, y_pool = np.class="kw">delete( X_pool, query_index, axis=class="num">0), np.class="kw">delete(y_pool, query_index) # Calculate and report our model&class="macro">#x27;s accuracy. model_accuracy = learner.score(X_raw, y_raw) print(&class="macro">#x27;Accuracy after query {n}: {acc:class="num">0.4f}&class="macro">#x27;.format( n=index + class="num">1, acc=model_accuracy)) # Save our model&class="macro">#x27;s performance for plotting. performance_history.append(model_accuracy) print(&class="macro">#x27;Score for the active learning with train size: &class="macro">#x27;, learner.X_training.shape) >>> learned = active_learner(pr, class="num">1000, class="num">1000, class="num">50) Score for the passive learning: class="num">0.5991245668429692 with train size: class="num">5483 Accuracy after query class="num">1: class="num">0.5710 Accuracy after query class="num">2: class="num">0.5836 Accuracy after query class="num">3: class="num">0.5749 Accuracy after query class="num">4: class="num">0.5847 Accuracy after query class="num">5: class="num">0.5829 Accuracy after query class="num">6: class="num">0.5823 Accuracy after query class="num">7: class="num">0.5650
主动学习委员会在 2000×8 样本上的精度拐点
把训练规模固定在 (2000, 8) 后,委员会式主动学习的验证精度从第 8 次查询的 0.5667 缓慢爬升,第 20 次查询达到 0.6002,中间在 13、15 次查询出现 0.5944、0.5949 两个局部高点。说明在外汇 / 贵金属特征维度仅 8 的情况下,靠不确定性批量采样补标样本,精度增益倾向边际递减,超过 15 次查询后每批带来的提升已不足 1 个百分点,属典型高风险小样本过拟合前兆。 下面这段 Python 是把已学得的 2000×8 训练集先做 GMM 聚成 75 类、再采样 1 万点造伪标签,喂给 CatBoost 做独立测试,相当于用生成数据压力测试委员会泛化能力。注意 GMM 的 covariance_type='full' 在 8 维上计算量可控,但 n_components=75 对 2000 原样本偏密,贵金属行情段切换时可能画出虚假聚类边界。 主动学习委员会本身由多个 AdaBoost 基学习器组成,每个用 max_depth=2 的决策树、50 棵、学习率 0.05;而对照的被动学习用 max_depth=3 的同构模型。代码里 committee.query 每次按 preset_batch 抽 batch_size 条最不确定样本,teach 时以 (1, -1) 形状单条注入,这种逐批在线更新在 MT5 外接 Python 服务里可直接复用,只是要把 X_pool 换成你从报价窗口抽的归一化特征。
Score for the active learning with train size: (class="num">2000, class="num">8) # prepare data for CatBoost catboost_df = pd.DataFrame(learned.X_training) catboost_df[&class="macro">#x27;labels&class="macro">#x27;] = learned.y_training # perform GMM clusterization over dataset X = catboost_df.copy() gmm = mixture.GaussianMixture( n_components=class="num">75, max_iter=class="num">500, covariance_type=&class="macro">#x27;full&class="macro">#x27;, n_init=class="num">1).fit(X) # sample new dataset generated = gmm.sample(class="num">10000) # make labels gen = pd.DataFrame(generated[class="num">0]) gen.rename(columns={gen.columns[-class="num">1]: "labels"}, inplace=True) gen.loc[gen[&class="macro">#x27;labels&class="macro">#x27;] >= class="num">0.5, &class="macro">#x27;labels&class="macro">#x27;] = class="num">1 gen.loc[gen[&class="macro">#x27;labels&class="macro">#x27;] < class="num">0.5, &class="macro">#x27;labels&class="macro">#x27;] = class="num">0 X = gen[gen.columns[:-class="num">1]] y = gen[gen.columns[-class="num">1]] pr = pd.DataFrame(X) pr[&class="macro">#x27;labels&class="macro">#x27;] = y # fit CatBoost model and test it model = fit_model(pr) test_model(model, TEST_START, END_DATE) def active_learner_committee(data, learners_number, labeled_size, unlabeled_size, batch_size): X_pool = data[data.columns[class="num">1:-class="num">1]].to_numpy() y_pool = data[data.columns[-class="num">1]].to_numpy() cl = AdaBoostClassifier(DecisionTreeClassifier(max_depth=class="num">3), n_estimators=class="num">50, learning_rate = class="num">0.05) cl.fit(X_pool, y_pool) print(&class="macro">#x27;Score for the passive learning: &class="macro">#x27;, cl.score( X_pool, y_pool), &class="macro">#x27; with train size: &class="macro">#x27;, data.shape[class="num">0]) # initializing Committee members learner_list = list() # Pre-set our batch sampling to retrieve class="num">3 samples at a time. preset_batch = partial(uncertainty_batch_sampling, n_instances=batch_size) for member_idx in range(learners_number): # initial training data train_idx = np.random.choice(range(X_pool.shape[class="num">0]), size=labeled_size, replace=False) X_train = X_pool[train_idx] y_train = y_pool[train_idx] # creating a reduced copy of the data with the known instances removed X_pool = np.class="kw">delete(X_pool, train_idx, axis=class="num">0) y_pool = np.class="kw">delete(y_pool, train_idx) # initializing learner learner = ActiveLearner( estimator=AdaBoostClassifier(DecisionTreeClassifier(max_depth=class="num">2), n_estimators=class="num">50, learning_rate = class="num">0.05), query_strategy=preset_batch, X_training=X_train, y_training=y_train ) learner_list.append(learner) # assembling the committee committee = Committee(learner_list=learner_list) unqueried_score = committee.score(X_pool, y_pool) performance_history = [unqueried_score] N_QUERIES = unlabeled_size class=class="str">"cmt">// batch_size for idx in range(N_QUERIES): query_idx, query_instance = committee.query(X_pool) committee.teach( X=X_pool[query_idx].reshape(class="num">1, -class="num">1), y=y_pool[query_idx].reshape(class="num">1, ) )
「委员会查询下的准确率曲线」
上面的代码段把每轮主动查询后的委员会模型准确率写进了 performance_history,并实时 print 出来,同时从样本池里删掉已被查询的实例,避免重复抽取。 从实际跑出来的日志看,被动学习在 5496 条训练样本下准确率约 0.6534,而主动学习前 5 轮查询后准确率反而掉到 0.5874 附近,明显低于被动基线。 往后查询到第 10 轮时出现 0.5934 的局部高点,但第 19 轮又滑落至 0.5732,整体在 0.57–0.59 区间震荡,没有稳定逼近被动学习的 0.65 水平。外汇与贵金属行情具有高噪声、非平稳特征,这类抽样策略在实盘迁移时过拟合概率偏高,属高风险实验。 开 MT5 接 Python 环境复跑这段逻辑,把 query 轮数拉到 50 看曲线是否收敛,比直接信面板数字更有意义。
model_accuracy = committee.score(X_pool, y_pool) performance_history.append(model_accuracy) print(&class="macro">#x27;Accuracy after query {n}: {acc:class="num">0.4f}&class="macro">#x27;.format( n=idx + class="num">1, acc=model_accuracy)) # remove queried instance from pool X_pool = np.class="kw">delete(X_pool, query_idx, axis=class="num">0) y_pool = np.class="kw">delete(y_pool, query_idx) class="kw">return committee >>> committee = active_learner_committee(pr, class="num">5, class="num">1000, class="num">1000, class="num">50) Score for the passive learning: class="num">0.6533842794759825 with train size: class="num">5496 Accuracy after query class="num">1: class="num">0.5927 Accuracy after query class="num">2: class="num">0.5818 Accuracy after query class="num">3: class="num">0.5668 Accuracy after query class="num">4: class="num">0.5862 Accuracy after query class="num">5: class="num">0.5874 Accuracy after query class="num">6: class="num">0.5906 Accuracy after query class="num">7: class="num">0.5918 Accuracy after query class="num">8: class="num">0.5910 Accuracy after query class="num">9: class="num">0.5820 Accuracy after query class="num">10: class="num">0.5934 Accuracy after query class="num">11: class="num">0.5864 Accuracy after query class="num">12: class="num">0.5753 Accuracy after query class="num">13: class="num">0.5868 Accuracy after query class="num">14: class="num">0.5921 Accuracy after query class="num">15: class="num">0.5809 Accuracy after query class="num">16: class="num">0.5842 Accuracy after query class="num">17: class="num">0.5833 Accuracy after query class="num">18: class="num">0.5783 Accuracy after query class="num">19: class="num">0.5732 Accuracy after query class="num">20: class="num">0.5828
◍ 主动学习在实盘前的几个清醒认知
主动学习用一小撮标注样本去撬动大模型,对部分分类问题确实有效,但它离真正自主的 AI 还差得远。这类模型在垃圾特征或噪声标签里挖不出稳定模式,特征和标签的准备仍高度依赖专家介入。 实测下来,模型质量并没有肉眼可见的提升,反而标注和训练的劳动强度明显变大——这是它落地到 MT5 策略前的硬伤。喜欢它的人多半是看中那种模拟人类挑重点思考的哲学,而不是省事。 附件里的 semi-supervised_catboost.py 导出的模型已能在新数据上跑出不错结果,有读者把模型转进 MQL5 后反馈外汇新品种泛化尚可。但 EURUSD 上表现好,不代表其他货币对直接复用参数也稳,跨品种须重做特征边界。 别把正态当圣经 文中置信度最低点的循环挑选没有固定未标注/已标注比例,也没有强制指标,本质是实验性玩法。直接套用前,先在 EURUSD 的 H1 上跑通再谈迁移。
class="type">class="kw">double catboost_model(const class="type">class="kw">double &features[]) { unsigned class="type">int TreeDepth[class="num">161] = {class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, class="num">6, }; unsigned class="type">int TreeSplits[class="num">966] = {class="num">393, class="num">730, class="num">93, class="num">54, class="num">352, class="num">313, class="num">540, class="num">591, class="num">217, class="num">12, class="num">576, class="num">757, class="num">208, class="num">574, class="num">756, class="num">446, class="num">505, class="num">10, class="num">487, class="num">791, class="num">210, class="num">673, class="num">125, class="num">647, class="num">286, class="num">593, class="num">523, class="num">706, class="num">566, class="num">510, class="num">575, class="num">754, class="num">325, class="num">450, class="num">470, class="num">321, class="num">438, class="num">589, class="num">48, class="num">257, class="num">283, class="num">745, class="num">707, class="num">520, class="num">564, class="num">296, class="num">702, class="num">27, class="num">524, class="num">223, class="num">404, class="num">755, class="num">60, class="num">218, class="num">387, }; unsigned class="type">int BorderCounts[class="num">20] = {class="num">36, class="num">44, class="num">40, class="num">41, class="num">42, class="num">40, class="num">30, class="num">30, class="num">36, class="num">35, class="num">43, class="num">45, class="num">27, class="num">37, class="num">52, class="num">55, class="num">45, class="num">40, class="num">43, class="num">38};
记住这一条就够了
把模型权重落进 MT5,最笨也最稳的办法就是直接写死数组。上面那段 Borders[799] 存的是 799 个分裂阈值,LeafValues[10304] 则是 10304 个叶子输出值,两者配合才能还原一棵随机森林的推断路径。 注意 Borders 里混着 -0.0103 到 -0.0013 这类浮点边界,LeafValues 精度到了 1e-16,手动改一个小数点都可能让信号反身。外汇和贵金属杠杆高,这类黑盒输出只可作概率参考,实盘前务必用历史 tick 重跑一遍校验。 代码贴进去能跑只是第一步,真正值钱的是你愿不愿意把这组数换成自己品种的参数重训——模型不更新,边界就永远是别人的。
class="type">class="kw">float Borders[class="num">799] = {-class="num">0.0103283636 f, -class="num">0.00538144633 f, -class="num">0.00438116584 f, -class="num">0.00384822348 f, -class="num">0.00290416228 f, -class="num">0.00226776977 f, -class="num">0.00186691666 f, -class="num">0.00173427281 f, -class="num">0.00136242132 f, , -class="num">0.00866030902 f, -class="num">0.0083276052 f, -class="num">0.00821269862 f, -class="num">0.00758890808 f, -class="num">0.0072928248 f, -class="num">0.00716711534 f, -class="num">0.00640411209 f, -class="num">0.00561416801 f, -class="num">0.0053433096 f, }; class=class="str">"cmt">/* 树叶值的聚合数组。每棵树用一行表示:*/ class="type">class="kw">double LeafValues[class="num">10304] = { -class="num">0.02908022041210655, class="num">0, -class="num">0.005608946748068618, class="num">0.005129329514937164, class="num">0.03600027378169195, class="num">0, class="num">0.02578289590577986, class="num">0.09444611655822675, class="num">0.03646431117733154, class="num">0.09977346533319338, -class="num">0.05595880296318598, -class="num">0.069314407568676, class="num">0.08718389822649918, -class="num">0.1200338438496052, class="num">0.0693147185156002, class="num">0.01000834600443637, class="num">0, class="num">0.06059264820464737, ,