直推和主动机器学习中的梯度提升(基础篇)
◍ 梯度提升在 MT5 里的两种落地路径
梯度提升(Gradient Boosting)在 MT5 的机器学习实践中分两条线:一条是直推式(transductive),用全部可见样本直接建模;另一条是主动式(active),让模型自己挑最有信息量的样本去问。两者都跑在 MetaTrader 5 的 Python/R 桥接或自建 DLL 上,不靠平台内置指标。 直推式更适合历史样本充足、行情结构稳定的贵金属横盘段;主动式在外汇直盘突变前的概率捕获上更省算力。2021 年 3 月社区原贴有 2033 次浏览、23 条讨论,说明这条路线在量化交易者里已有真实需求。 开 MT5 接 Python 环境,先用小样本验证主动式标注逻辑,比直接堆全量梯度提升树更不容易过拟合。外汇与贵金属杠杆高、滑点大,任何模型信号都只是概率倾向,需实盘前做样本外压力测试。
半监督与主动学习在行情建模里的位置
半监督学习把少量已标注样本和大量未标注样本放在一起训练,先在小块已知区域上拟合,再把结构外推到大片未知区域。瓦普尼克当年提出的直推思路很直接:别先去解“所有情况”的泛化函数,先拿到你真正要的那一类判别边界,过度拟合的概率会低一些。外汇与贵金属波动里,这种思路能缓解样本标注贵、标注少的问题,但杠杆市场高风险,外推结论只代表历史结构倾向。 要让未标注数据真正起作用,至少得踩中一条假设:连续性(邻近点更可能同标签,边界偏好走低密度区)、聚类(数据成簇,同簇同标签倾向)、或流形(数据实际落在低维曲面上,可避开通胀式的维度灾难)。我在前篇用标记数据密度估计再采样,新数据分布一偏移就露怯;半监督补的正是这块,后文实验会给出具体精度差异。 主动学习是半监督的自然延续:模型自己挑“最该被标注”的点交给 oracle。三种场景里,基于池的采样最常用——把未标注池按信息量排个序,每次捞最不确定的 N 个进训练集。查询策略上,不确定度抽样看单类概率低于边界,边际抽样看前两类的差,熵采样直接算信息熵最大值。整套循环:标数据训模型→伪标未标注→按策略选点→补训练集→重复到误差或迭代停。MT5 上你可以先拿 EURUSD 的 H1 形态做少量标注,跑一轮池采样看边界变化。
「用 modAL 把随机森林变成会挑样本的主动学习者」
主动学习的核心不是模型多复杂,而是让模型自己指出「哪些未标记样本最该被人工标注」。Python 里 modAL、Libact、Alipy 都能做这件事,modAL 因为和 scikit-learn 完全兼容、上手直观,适合先跑通理念。 它的最大便利是能直接套用任意 scikit-learn 分类器。下面这段用随机森林当学习器兼评估器,查询策略选 entropy_sampling(基于熵),意味着模型会优先挑预测熵最高的样本请人工标签,初始只用一小批已标数据冷启动。 modAL 还允许把多种不确定性度量做线性组合或乘积,生成复合策略。代码里 linear_combination 是 uncertainty 与 margin 等权相加,product 则是两者分别取 0.5 和 0.1 次幂相乘,再用 multi_argmax 从候选区捞 top-n。 外汇与贵金属行情噪声大、标注贵,这种挑样本方式可能把有限标注预算花在边界案例上,但高频噪声下仍可能选到无效样本,属高风险实验。
from modAL.models class="kw">import ActiveLearner from modAL.uncertainty class="kw">import entropy_sampling from sklearn.ensemble class="kw">import RandomForestClassifier learner = ActiveLearner( estimator=RandomForestClassifier(), query_strategy=entropy_sampling, X_training=X_training, y_training=y_training ) from modAL.utils.combination class="kw">import make_linear_combination, make_product from modAL.uncertainty class="kw">import classifier_uncertainty, classifier_margin # creating new utility measures by linear combination and product # linear_combination will class="kw">return class="num">1.0*classifier_uncertainty + class="num">1.0*classifier_margin linear_combination = make_linear_combination( classifier_uncertainty, classifier_margin, weights=[class="num">1.0, class="num">1.0] ) # product will class="kw">return (classifier_uncertainty**class="num">0.5)*(classifier_margin**class="num">0.1) product = make_product( classifier_uncertainty, classifier_margin, exponents=[class="num">0.5, class="num">0.1] ) from modAL.utils.selection class="kw">import multi_argmax # defining the custom query strategy, which uses the linear combination of # classifier uncertainty and classifier margin def custom_query_strategy(classifier, X, n_instances=class="num">1): utility = linear_combination(classifier, X) query_idx = multi_argmax(utility, n_instances=n_instances) class="kw">return query_idx, X[query_idx] custom_query_learner = ActiveLearner( estimator=GaussianProcessClassifier(class="num">1.0 * RBF(class="num">1.0)), query_strategy=custom_query_strategy, X_training=X_training, y_training=y_training )
◍ 主动学习里的三种抽样逻辑
做半监督训练时,挑哪条未标记样本去打标签直接决定了模型迭代效率。核心思路都绕着“不确定性”转:最朴素的是分类不确定性 U(x)=1−P(x^|x),也就是拿 1 减去最可能的那类概率;三个样本 [0.1,0.85,0.05]、[0.6,0.3,0.1]、[0.39,0.61,0.0] 算出来不确定度分别是 0.15、0.4、0.39,第二个样本最该优先标。
| 分类裕度看的是第一、第二可能类的概率差 M(x)=P(x1^ | x)−P(x2^ | x),差越小越模糊。上面那组数里第三行差仅 0.22,比前两行的 0.75、0.3 都小,所以批量选边界样本时会先捞它。 |
|---|
分类熵走信息熵 H(x)=−∑k pk log(pk),分布越平熵越大。同一组样本熵值依次为 0.518、0.898、0.669,第二个依旧最不确定,和不确定度口径一致。 一次只查一个再重训太慢,排序集抽样按余弦距离估特征空间被探索的程度,每轮把等级最高的拽进训练池再重算,直到凑满 batch。信息密度度量补了简单策略不看数据结构的坑,用余弦或欧氏距离找和周围最像的实例。委员会查询则并行养多个假设,专挑分歧大的样本,能压掉单分类器的偏好偏差,bagging 和 bootstrapping 都算这类思路。 下面这段 numpy 演示了从概率矩阵算不确定度、裕度、熵的全过程,开 MT5 旁边的 Python 环境粘过去就能复现那几个数字。
[[class="num">0.1 , class="num">0.85, class="num">0.05], [class="num">0.6 , class="num">0.3 , class="num">0.1 ], [class="num">0.39, class="num">0.61, class="num">0.0 ]] class="num">1 - proba.max(axis=class="num">1) [class="num">0.15, class="num">0.4 , class="num">0.39] >>> class="kw">import numpy as np >>> proba = np.array([[class="num">0.1 , class="num">0.85, class="num">0.05], ... [class="num">0.6 , class="num">0.3 , class="num">0.1 ], ... [class="num">0.39, class="num">0.61, class="num">0.0 ]]) >>> >>> proba array([[class="num">0.1 , class="num">0.85, class="num">0.05], [class="num">0.6 , class="num">0.3 , class="num">0.1 ], [class="num">0.39, class="num">0.61, class="num">0.0 ]]) >>> part = np.partition(-proba, class="num">1, axis=class="num">1) >>> part array([[-class="num">0.85, -class="num">0.1 , -class="num">0.05], [-class="num">0.6 , -class="num">0.3 , -class="num">0.1 ], [-class="num">0.61, -class="num">0.39, -class="num">0.0 ]]) >>> part[:, class="num">0] array([-class="num">0.85, -class="num">0.6 , -class="num">0.61]) >>> part[:, class="num">1] array([-class="num">0.1 , -class="num">0.3 , -class="num">0.39]) >>> margin = - part[:, class="num">0] + part[:, class="num">1] >>> margin array([class="num">0.75, class="num">0.3 , class="num">0.22]) [class="num">0.51818621, class="num">0.89794572, class="num">0.66874809]