第 8 章 · 01 AbuML 三层架构与学习器工厂 本节摘要:本节是第 8 章的地基——精读 ABU 机器学习模块的三层架构。底层 (918 行)封装 sklearn 全部常用学习器,每个学习器都提供 实例化方法和 网格调参方法,统一由 入口驱动;中间层 (1896 行)持有 x/y/df 三件套,用 装饰器声明每个方法支持的学习器类型(support),再用 自动判断分类还是回归( → 分类,否则回归),不在 support 列表就打 log 优雅返回;顶层 (289 行)是业务层,子类在 里把金融数据切成 x/y/df,基类自动构造 ,再用 把所有方法调用透传给中间层——业务对象可以像调 AbuML 一样直接调 。
本节摘要:本节是第 8 章的地基——精读 ABU 机器学习模块的三层架构。底层
AbuMLCreater(918 行)封装 sklearn 全部常用学习器,每个学习器都提供xxx(assign=True, **kwargs)实例化方法和xxx_best(x, y, param_grid)网格调参方法,统一由_estimators_prarms_best入口驱动;中间层AbuML(1896 行)持有 x/y/df 三件套,用entry_wrapper装饰器声明每个方法支持的学习器类型(support),再用E_FIT_AUTO自动判断分类还是回归(len(np.unique(y)) <= 10→ 分类,否则回归),不在 support 列表就打 log 优雅返回;顶层AbuMLPd(289 行)是业务层,子类在make_xy里把金融数据切成 x/y/df,基类自动构造self.fiter = AbuML(...),再用__getattr__把所有方法调用透传给中间层——业务对象可以像调 AbuML 一样直接调close_predict.fit()。读完本节你将理解 ABU 是怎么把 sklearn 的"裸 API"包装成"金融业务能直接用"的 ML 工厂。
内容来源:原项目源码
abupy/MLBu/ABuMLPd.py+abupy/MLBu/ABuML.py+abupy/MLBu/ABuMLCreater.py+abupy/MLBu/ABuMLGrid.py,精读并套用体系化模板。
⚠️ 注意:本节是第 8 章的基础,后续两节(概率阈值搜索 + 比特币案例)都建立在本节的三层架构之上。注意 ABU 把"分类 vs 回归"的判断藏在装饰器里(而不是构造参数里),这是它最巧妙的设计——同一份业务代码可以无缝切换学习器类型。
阅读完本节,你应当能够:
AbuMLPd.make_xy 的契约(必须设置 self.x/self.y/self.df)与 __getattr__ 的方法代理原理。entry_wrapper 装饰器的三步逻辑(pop fiter_type → E_FIT_AUTO 归约到 CLF/REG → 检查 support 决定执行或打 log)。len(np.unique(y)) <= 10 → 分类)及其设计动机。xxx(assign, **kwargs) 实例化 vs xxx_best(x, y, param_grid) 调参,以及 _estimators_prarms_best 如何统一这两条路径。AbuMLGrid._scoring_grid 的分类/回归度量分流(accuracy vs explained_variance_score)与 signature 参数健壮性检查。ABU 的机器学习模块刻意做成三层,每层职责单一、互不耦合。数据流自上而下,学习器自下而上:
三层的职责分工:
| 层 | 类 | 行数 | 职责 |
|---|---|---|---|
| 业务层 | AbuMLPd(抽象) | 289 | 子类 make_xy 生产 x/y/df;__getattr__ 代理方法 |
| 中间层 | AbuML | 1896 | 持有 x/y/df;entry_wrapper 控制分类/回归分发;提供 fit/predict/cross_val/plot |
| 工厂层 | AbuMLCreater | 918 | 实例化 sklearn 全部常用学习器;每个学习器双方法 xxx + xxx_best |
| 工具层 | AbuMLGrid | 378 | 网格搜索;signature 检查参数;分类/回归度量分流 |
业务层不直接调 sklearn,中间层不直接 new 学习器(只通过 self.estimator = AbuMLCreater()),工厂层不持有数据(只接受外部传入的 x/y)。这种"数据向下、对象向上"的解耦,让 UMP 裁判、比特币分类、泰坦尼克示例都能复用同一套 ML 流水线。
AbuMLPd.py:24-66 定义了业务层抽象基类:
24 class AbuMLPd(six.with_metaclass(ABCMeta, object)): 25 """封装AbuML的上层具体业务逻辑类""" 26 27 def __init__(self, **kwarg): 28 """ 29 从kwarg中输入数据或者,make_xy中本身生产数据,在做完 30 make_xy之后,类变量中一定要有x,y和df,使用AbuML继续 31 构造self.fiter 32 """ 33 self.make_xy(**kwarg) 34 if not hasattr(self, 'x') or not hasattr(self, 'y') \ 35 or not hasattr(self, 'df'): 36 raise ValueError('make_xy failed! x, y not exist!') 37 # noinspection PyUnresolvedReferences 38 self.fiter = AbuML(self.x, self.y, self.df) 39 40 @abstractmethod 41 def make_xy(self, **kwarg): 42 """子类需要完成的abstractmethod方法""" 43 pass 44 45 def __getattr__(self, item): 46 """ 47 使用ABuML对象self.fiter做为方法代理: 48 return getattr(self.fiter, item) 49 即AbuMLPd中可以使用ABuML类对象中任何方法 50 """ 51 if item.startswith('__'): 52 # noinspection PyUnresolvedReferences 53 return super().__getattr__(item) 54 return getattr(self.fiter, item) 55 56 def __call__(self): 57 """方便外面直接 call,不用每次去get""" 58 return self.fiter
构造流程三步:
self.x(numpy 特征矩阵)、self.y(numpy 标签)、self.df(pandas DataFrame)。缺一个就 raise ValueError——这是业务层与中间层的硬契约。self.fiter = AbuML(self.x, self.y, self.df),把三件套交给中间层。💡 解剖要点:
make_xy是模板方法模式(Template Method)的标准实现——基类__init__固化流程(先 make_xy 再校验再构造 fiter),子类只填空。ABU 把这种"流程固化在基类、细节留给子类"的写法贯穿全书(买入因子的_init_self、UMP 裁判的get_predict_col都是同一套路)。
__getattr__(45-54 行)是 Python 的属性拦截钩子。它只在常规属性查找失败时触发。关键两步:
if item.startswith('__'):dunder 方法(如 __deepcopy__、__getstate__)走 super().__getattr__,避免无限递归(否则又会回到 self.fiter 查找)。return getattr(self.fiter, item):其他属性一律转发给中间层。效果惊人——业务层对象 close_predict = ClosePredict() 之后,可以直接写 close_predict.fit()、close_predict.plot_decision_function()、close_predict.feature_selection(),仿佛这些方法就定义在 ClosePredict 上。实际上 ClosePredict 自己一个 ML 方法都没写,全靠 __getattr__ 透传。
__call__(56-58 行)是糖:close_predict() 等价于 close_predict.fiter,方便外部拿到中间层对象做高级操作。
中间层 AbuML.py:115 持有三件套 + 一个学习器工厂:
115 class AbuML(object): 116 """封装有简单及无监督学习方法以及相关操作类""" 341 def __init__(self, x, y, df, fiter_type=EMLFitType.E_FIT_AUTO): 351 self.estimator = AbuMLCreater() 355 self.x = x 356 self.y = y 357 self.df = df 359 self.log_func = logging.info if ABuEnv.g_is_ipython else print 360 self.fiter_type = fiter_type
fiter_type 默认 E_FIT_AUTO——这是 ABU 最关键的设计:让装饰器自动判断分类还是回归。EMLFitType 枚举(ABuML.py:52-67)声明所有支持的学习器类型:
52 class EMLFitType(Enum): 53 """支持常使用的学习器类别enum""" 54 """有监督学习:自动选择,根据y的label数量,> 10使用回归否则使用分类""" 55 E_FIT_AUTO = 'auto' 56 """有监督学习:回归""" 57 E_FIT_REG = 'reg' 58 """有监督学习:分类""" 59 E_FIT_CLF = 'clf' 60 """无监督学习:HMM""" 62 E_FIT_HMM = 'hmm' 63 """无监督学习:PCA""" 65 E_FIT_PCA = 'pca' 66 """无监督学习:KMEAN""" 67 E_FIT_KMEAN = 'kmean'
六个值分两组:有监督(AUTO/REG/CLF)、无监督(HMM/PCA/KMEAN)。AUTO 是个"占位符",实际执行前会被归约成 CLF 或 REG。
ABuML.py:70-111 是全章最关键的装饰器:
70 def entry_wrapper(support=(EMLFitType.E_FIT_CLF, EMLFitType.E_FIT_REG, EMLFitType.E_FIT_HMM, 71 EMLFitType.E_FIT_PCA, EMLFitType.E_FIT_KMEAN)): 72 """ 73 类装饰器函数,对关键字参数中的fiter_type进行标准化,eg,fiter_type参数是'clf', 74 转换为EMLFitType(fiter_type)赋予self.fiter_type,检测当前使用的具体学习器不在support参数中 75 不执行被装饰的func函数了,打个log返回 76 """ 77 def decorate(func): 78 @functools.wraps(func) 79 def wrapper(self, *args, **kwargs): 80 org_fiter_type = self.fiter_type 81 if 'fiter_type' in kwargs: 82 # 如果传递了fiter_type参数,pop出来 83 fiter_type = kwargs.pop('fiter_type') 84 if isinstance(fiter_type, six.string_types): 85 fiter_type = EMLFitType(fiter_type) 86 self.fiter_type = fiter_type 87 88 check_support = self.fiter_type 89 if self.fiter_type == EMLFitType.E_FIT_AUTO: 90 # 把auto的归到具体的分类或者回归 91 check_y = self.y 92 if 'y' in kwargs: 93 check_y = kwargs['y'] 94 check_support = EMLFitType.E_FIT_CLF if len(np.unique(check_y)) <= 10 else EMLFitType.E_FIT_REG 95 if check_support not in support: 96 # 当前使用的具体学习器不在support参数中不执行被装饰的func函数了,打个log返回 97 self.log_func('{} not support {}!'.format(func.__name__, check_support.value)) 98 # 如果没能成功执行把类型再切换回来 99 self.fiter_type = org_fiter_type 100 return 101 return func(self, *args, **kwargs)
三步逻辑:
fiter_type='clf',pop 出来转成 EMLFitType('clf'),临时赋给 self.fiter_type。这让你可以单次调用切换学习器:ttn_abu.feature_selection(fiter_type='kmean')。E_FIT_AUTO 不能直接执行,必须先归约。规则就一句——len(np.unique(y)) <= 10 就当分类,否则当回归。10 是 ABU 的经验阈值:离散标签(0/1/2 这种,或者周几 0-6)通常 ≤10 种,连续值(收盘价、profit_cg)远超 10 种。注意 92-93 行:如果 kwargs 里临时传了 y,优先用 kwargs 的 y 判断(配合单次调用切换数据)。check_support 必须在被装饰方法的 support 声明里。比如 cross_val_prob_accuracy_score 声明 support=(EMLFitType.E_FIT_CLF,)(只支持分类),如果当前是回归就打 log 返回 None——优雅降级,不抛异常。100 行恢复 org_fiter_type,保证本次调用不污染后续状态。AbuML 的每个方法都按"自身能力"声明 support:
| 方法 | support 声明 | 含义 |
|---|---|---|
cross_val_accuracy_score |
(E_FIT_CLF,) |
只支持分类 |
cross_val_silhouette_score |
(E_FIT_KMEAN,) |
只支持聚类 |
train_test_split_xy |
(E_FIT_CLF,) |
切分+混淆矩阵,只分类 |
cross_val_mean_squared_score |
(E_FIT_CLF, E_FIT_REG) |
分类回归都支持 |
fit / fit_transform / predict |
() 全支持 |
通用 |
feature_selection |
() 全支持 |
RFE(监督)/VarianceThreshold(无监督)内部分流 |
support 默认值(L70-71)是除 AUTO 外的全部五种——即"被装饰的方法默认啥都支持",具体方法按需收紧。这种"白名单声明"让代码自文档化:看一眼装饰器就知道这个方法能用在什么场景。
💡 解剖要点:
entry_wrapper把"判断分类还是回归"这个本该写在每个方法开头的重复逻辑,统一收口到装饰器里。更深一层,E_FIT_AUTO 让业务层完全不用关心学习器类型——BtcBigWaveClf的 y 是 0/1(big_wave),自动归约成 CLF;ClosePredict的 y 是收盘价(连续),自动归约成 REG。一份业务代码,两套学习器,零改动。
get_fiter(ABuML.py:399-442)是装饰器之外另一处把 AUTO 落地的地方——每次真正要拿学习器对象时调用:
399 def get_fiter(self): 417 if self.fiter_type == EMLFitType.E_FIT_AUTO: 418 if len(np.unique(self.y)) <= 10: 419 # 小于等于10个class的y就认为是要用分类了 420 fiter = self.estimator.clf 421 else: 422 fiter = self.estimator.reg 423 elif self.fiter_type == EMLFitType.E_FIT_REG: 424 fiter = self.estimator.reg 425 elif self.fiter_type == EMLFitType.E_FIT_CLF: 426 fiter = self.estimator.clf 427 elif self.fiter_type == EMLFitType.E_FIT_HMM: 428 if self.estimator.hmm is None: 429 self.estimator.hmm_gaussian() 430 fiter = self.estimator.hmm
注意 427-430 行的无监督分支:self.estimator.hmm 默认是 None(工厂构造时不初始化),第一次用才懒加载 hmm_gaussian()。PCA、KMeans 同理。这是性能优化——HMM/PCA/KMeans 用得少,不预先实例化节省内存。有监督的 clf/reg 在工厂构造时就默认初始化(__init__ 里 self.reg = self.linear_regression(); self.clf = self.svc(probability=True)),因为几乎必用。
is_supervised_learning(ABuML.py:362-368)是配套的判断函数:AUTO/REG/CLF 都算监督,HMM/PCA/KMEAN 算无监督。fit 方法用它来决定 fit(x, y) 还是 fit(x)(无监督没有 y)。
工厂层把 sklearn 全部常用学习器封装成统一的双方法形式。以随机森林为例(ABuMLCreater.py:587-638):
587 def random_forest_classifier(self, assign=True, **kwargs): 588 """有监督学习分类器,实例化RandomForestClassifier""" 609 # 无 param_grid 时调 random_forest_classifier_best 610 if kwargs is not None and len(kwargs) > 0: 611 # 透传 kwargs 612 clf = RandomForestClassifier(**kwargs) 613 else: 614 # 默认参数 615 clf = RandomForestClassifier(n_estimators=100, max_features='sqrt', random_state=1) 616 if assign: 617 self.clf = clf 618 return clf 619 620 def random_forest_classifier_best(self, x, y, param_grid=None, assign=True, n_jobs=-1, show=True): 621 """寻找RandomForestClassifier构造器的最优参数""" 622 return self._estimators_prarms_best(self.random_forest_classifier, x, y, param_grid, assign, n_jobs, show)
每个学习器严格遵守这个约定:
xxx(assign=True, **kwargs):实例化方法。assign=True 把实例化对象存到 self.clf/self.reg/self.hmm/self.pca/self.kmean(取决于学习器类别),assign=False 只返回临时对象不存。kwargs 有值就透传给 sklearn 类(XxxClassifier(**kwargs)),无值用 ABU 调好的默认参数。xxx_best(x, y, param_grid, ...):网格调参方法。固定签名,统一委托给 _estimators_prarms_best。工厂封装的学习器(共 14 个,覆盖 sklearn 主流):
| 类别 | 学习器 | best 默认 callback |
|---|---|---|
| 分类 | svc / bagging_classifier / adaboost_classifier / random_forest_classifier / decision_tree_classifier / knn_classifier / xgb_classifier / logistic_classifier | n_estimators / max_depth / n_neighbors |
| 回归 | linear_regression / polynomial_regression / bagging_regressor / adaboost_regressor / random_forest_regressor / decision_tree_regressor / xgb_regressor | n_estimators / max_depth |
| 无监督 | pca_decomposition / kmean_cluster / hmm_gaussian | (无 best) |
| 多分类包装 | onevsone_classifier / onevsreset_classifier | (无 best) |
ABuMLCreater.py:151-182 是所有 xxx_best 的统一入口:
151 def _estimators_prarms_best(self, create_func, x, y, param_grid, assign, n_jobs, show, 152 grid_callback=ABuMLGrid.grid_search_init_n_estimators): 170 # 通过create_func创建一个示例学习器,assign=False 171 estimator = create_func(assign=False) 172 if param_grid is not None and isinstance(param_grid, dict): 173 # 有 param_grid,使用grid_search_mul_init_kwargs寻找参数最优值 174 _, best_params = ABuMLGrid.grid_search_mul_init_kwargs(estimator, x, y, 175 param_grid=param_grid, n_jobs=n_jobs, show=show) 176 else: 177 # 没有 param_grid,使用学习器对应的 grid_callback 178 _, best_params = grid_callback(estimator, x, y, show=show) 179 if best_params is not None: 180 # 用最优参数重新构造学习器 181 return create_func(assign=assign, **best_params)
两条调参路径:
grid_search_mul_init_kwargs 做多参数联合网格搜索。比如 param_grid = {'max_depth': np.arange(2, 5), 'n_estimators': np.arange(100, 300, 50)}。grid_callback。callback 是 ABU 为每个学习器预置的"该调哪个参数"的经验——随机森林/AdaBoost/Bagging 默认搜 n_estimators(树的数量),决策树默认搜 max_depth,KNN 默认搜 n_neighbors,PCA 默认搜 n_components。拿到最优参数后,180-181 行 create_func(assign=assign, **best_params) 用最优参数重新实例化并按需存到 self.clf/self.reg。这样上层 ttn_abu.random_forest_classifier_best() 一行调用,既完成网格搜索又把最优模型存到 self.estimator.clf,后续 get_fiter() 直接拿到。
AbuMLGrid.py:31-60 的 _scoring_grid 做分类/回归度量分流:
31 def _scoring_grid(estimator, scoring): 43 if not isinstance(estimator, (ClassifierMixin, RegressorMixin)): 44 logging.info('only support supervised learning') 45 return None 48 if scoring is None: 49 if isinstance(estimator, ClassifierMixin): 50 # 分类器使用accuracy 51 return 'accuracy' 52 elif isinstance(estimator, RegressorMixin): 53 # 回归器使用可释方差值explained_variance_score 54 return make_scorer(explained_variance_score, greater_is_better=True) 55 return None 56 return scoring
没传 scoring 时,分类用 accuracy(准确率),回归用 explained_variance_score(可释方差,用 make_scorer 包装成 scorer 对象)。无监督学习(既不是 ClassifierMixin 也不是 RegressorMixin)直接返回 None——ABU 的网格搜索暂不支持无监督。
grid_search_init_kwargs(ABuMLGrid.py:63-148)最关键的健壮性设计在 102-116 行:
103 init = getattr(estimator.__class__.__init__, 'deprecated_original', estimator.__class__.__init__) 104 # 获取函数签名 105 init_signature = signature(init) 112 if param_name not in init_signature.parameters.keys(): 113 # 如果需要grid的参数param_name不在init函数签名中,打log,返回 114 logging.info('check init signature {} not in **kwargs\ninit_signature:{}'.format( 115 param_name, init_name, init_signature.parameters.keys())) 116 return None, None
signature 拿学习器 __init__ 的形参列表,检查 param_name 在不在里面。比如 SVC 没有 n_estimators 参数,你硬要 grid n_estimators 会直接打 log 返回——优雅降级,不抛异常。103 行的 deprecated_original 是 sklearn 老版本兼容(SVC 的 __init__ 被 @deprecated 装饰过,真正的签名在 deprecated_original 上)。
四个预置 callback(ABuMLGrid.py:256-378)各自有自己的默认搜索范围:
| callback | 参数 | 默认范围 |
|---|---|---|
grid_search_init_n_estimators |
n_estimators | np.arange(50, 500, 10) |
grid_search_init_max_depth |
max_depth | np.arange(2, np.maximum(10, x.shape[1]-1), 1) |
grid_search_init_n_neighbors |
n_neighbors | np.arange(1, np.minimum(26, x.shape[0]/3), 1) |
grid_search_init_n_components |
n_components | np.arange(2, np.maximum(10, x.shape[1]-1), 1) |
注意范围会自适应数据:max_depth/n_components 上限跟特征列数 x.shape[1] 挂钩,n_neighbors 上限跟样本数 x.shape[0] 挂钩(KNN 邻居数不能超过样本数的 1/3,否则退化成多数投票)。
💡 解剖要点:AbuMLGrid 的
signature检查是 ABU "防御式编程"的典型体现。sklearn 学习器参数差异巨大(SVC 有 C/kernel,RF 有 n_estimators,PCA 有 n_components),硬编码参数列表很容易踩雷。用signature反射,既统一了入口,又避免了"传错参数报错"的尴尬——参数不存在就降级打 log,grid 流程不中断。这种"宁可降级不报错"的风格,在量化回测里特别重要:回测要跑几十个学习器,一个报错就全盘崩溃。
__getattr__ 代理)→ 中间层 AbuML(持有 x/y/df + entry_wrapper 分发)→ 工厂层 AbuMLCreater(封装 sklearn + 双方法约定)。数据向下、对象向上,完全解耦。make_xy 并设置 self.x/self.y/self.df,基类 __init__ 校验三件套后自动构造 self.fiter = AbuML(x, y, df)。__getattr__ 把非 dunder 属性全部透传给 fiter,业务对象可直接调 AbuML 全部方法。len(np.unique(y)) <= 10 → CLF 否则 REG)→ support 检查(不在白名单打 log 优雅返回,不抛异常)。is_supervised_learning 判断 fit(x,y) 还是 fit(x)。xxx(assign=True, **kwargs) 实例化 + xxx_best(x, y, param_grid) 调参。assign=True 存到 self.clf/self.reg。grid_search_mul_init_kwargs 多参数联合搜索;无 param_grid 用学习器自带 grid_callback(随机森林搜 n_estimators,决策树搜 max_depth,KNN 搜 n_neighbors)。_scoring_grid 分类 accuracy / 回归 explained_variance_score / 无监督 None。signature 检查 param_name 在学习器 __init__ 签名里,不存在则打 log 返回,防御式编程。callback 默认范围自适应 x.shape。下一节,我们深入 AbuML 的"概率阈值搜索"——
cross_val_prob_accuracy_score(score = effect_rate × accuracy,准确率与生效率的权衡)、search_match_pos_threshold(linspace 找最优阈值)、predict_proba_threshold(概率 ≥ threshold → 1,否则返回 default_ret,这是 UMP 拦截的 ML 核心)。