auto-sklearn源码阅读(1):超参空间的构造

本文分为软核与硬核部分,软核部分适合一般读者,为科普内容,不直接涉及代码。
硬核部分适合想研究底层源码的算法工程师

Auto-Sklearn

软核部分

源码哥的烦恼

这天,源码哥的老板丢给源码哥一个Excel表,说:

“小猿,这里有两个表,一个表是上个月用户的行为特征和留存情况,一个是这个月的用户特征,写个算法预测一下这个月的用户留存率,下班前给我。”

只见源码哥推了推眼镜,撸了撸衣袖,心想:不就是一个二分类监督学习任务吗?这有何难?Jupyter启动

可是源码哥打开Excel就犯了难,表格中有些用户的特征是有缺失的。只见源码哥手忙脚乱地用sklearn.impute.SimpleImputer处理完缺失值,又开始为用哪个分类器去拟合数据犯了难:”XGBoost好,SVM快,到底用哪个呢?“

源码哥靠丢硬币选择了一个模型,又开始肝起了超参:”SVM光是kernel就有4个,该从哪个开始呢?“

只见源码哥把键盘一摔,说:”这破程,老子不编了!Jupyter关闭!“


auto-sklearn的功能与作用域

虽然上文只是个段子,但这也许就是许多数据科学家和算法工程师的日常:数据预处理(缺失值处理、category数据独热码编码),特征预处理(特征缩放,重要特征选择,数据降维),模型选择(SVM,RandomForest),模型超参数优化。

那么问题来了,有什么方法能将上述过程自动呢?下面,就有请本文今天的主角登场:auto-sklearn!

auto-sklearn(https://github.com/automl/auto-sklearn)是automl团队(http://www.automl.org/)于2015年推出的一款自动化机器学习框架,它的功能在Tabular Data的数据上,构件一条囊括 数据处理、特征处理、模型选择,模型超参优化的自动化机器学习Pipeline,简单来说,就是给他一个菜单,他自己会买菜,切菜,烹饪,最后把菜端到你面前让你品尝的贴心管家

auto-sklearn的作用域,就是Tabular Data上的监督学习

机器学习的几种分类

Tabular Data: 每一行为一个样本,每列为特征。如果每个样本都有对应的标签(label),为监督学习。如果每个样本没有标签,为无监督学习。如果标签是连续的,例如根据一些特征预测某地的房价,称为回归任务。如果标签是离散的,例如根据一些特征判断西瓜的好坏,称为分类任务


从一个例子入手

源码哥在关注了人工智能源码阅读(aicodereview)公众号,看了这篇文章之后,不禁欣喜若狂:

”自从有了auto-sklearn,妈妈再也不用担心我的模型!“

于是源码哥在ipython中按照官方文档的介绍写下了如下代码:

>>> import autosklearn.classification
>>> import sklearn.model_selection
>>> import pandas as pd
>>> import sklearn.metrics
>>> df = pd.read_excel("上月用户留存情况.xlsx")
>>> y = df.pop("用户是否留存").values
>>> X = df.values
>>> X_train, X_test, y_train, y_test = \
        sklearn.model_selection.train_test_split(X, y, random_state=1)
>>> automl = autosklearn.classification.AutoSklearnClassifier()
>>> automl.fit(X_train, y_train)
>>> y_hat = automl.predict(X_test)
>>> print("Accuracy score", sklearn.metrics.accuracy_score(y_test, y_hat))

Document Example: https://automl.github.io/auto-sklearn/master/#example

看着电脑屏幕上模型的指标不断地提升,源码哥不禁畅想起了未来:

出任CTO,迎娶白富美,走上人生巅峰…

auto-sklearn 的 Pipeline

第二天,在得到了老板的表扬之后,源码哥不禁对auto-sklearn的底层原理产生了兴趣。到底是什么样的代码,能将数据和模型治理得服服帖帖呢?

在阅读了auto-sklearn的论文(http://papers.nips.cc/paper/5872-efficient-and-robust-automated-machine-learning.pdf)之后,源码哥说:”auto-sklearn也没什么神奇的嘛,不就是个水管工吗
Pipeline的优化过程
为什么这么说呢?因为如果把数据想象成自来水,把数据预处理、特征预处理、估计器都想象成水管,模型的表现想象成水的流速,问题其实很简单,那就是选择什么样的水管(算法选择),将水管上的阀门拧到什么样的位置(超参选择),能让水流的最快。

但是两个不同的水管也许单独用起来都不咋地,但是拼在一起都奏效了,这就是一个组合问题了。

上图为论文中的图片,我们可以看到,数据的balacing我们可以选择做与不做,缺失值填充可以选择用中位数(median)或者平均数(mean)填充… ,最后的那根”管子“:估计器(estimator)是一根特殊的管子,他不仅要”选择什么样的水管“(算法选择),还要知道”将水管上的阀门拧到什么样的位置“(超参选择)。

优化过程:贝叶斯优化

搞懂了Pipeline的构建过程,源码哥不禁开始思考另外一个问题:这么多参数组合在一起,搜索空间将是一个非常巨大的空间,会出现组合爆炸的问题,auto-sklearn难道会像瞎猫抓耗子一样,到处乱撞吗?

源码哥的同事,算法大神小聪看出了源码哥的疑虑,对他说:当然不会随机搜索了,在auto-sklearn中,过去的搜索会对决定未来的搜索方向的。

小聪说,auto-sklearn用的是smac(https://github.com/automl/SMAC3)算法,是贝叶斯优化算法的一种。算法在刚初始化时,的确类似随机搜索,但是随着搜索的进行,算法知道的信息越来越多,就像诸葛亮一样,能预知下一次搜索哪个点模型的表现会最好。

在这里插入图片描述

来源: borgwang.github.io

小聪拿出一幅图对源码哥说:图中浅蓝色的表示95%置信区间的上下界,越宽表示对某个点预测的标准差越大,表示对这个点越不确定,就像是个臭皮匠。但随着搜索过的点越来越多,历史点(红叉)附近的标准差就会降低,表示对附近的点越确定,就像是个诸葛亮。就这样,三个臭皮匠,凑成一个诸葛亮,这个诸葛亮会拟合出一个参数空间映射到模型表现的函数,从这个空间中找一个点,作为下次的搜索点。

源码哥恍然大悟。但他还是追问了一句:这些都具体是怎么实现的呢?

小聪说:看完硬核部分,你就懂了。

硬核部分

基本运行入口:autosklearn.automl.AutoMLClassifier#fit
在子类配置了一些必要的参数之后,调用父类的fit方法,即autosklearn.automl.AutoML#fit
在调用loaded_data_manager = XYDataManager(...将X y进行管理之后,调用return self._fit(...

创建搜索空间

self.configuration_space, configspace_path = self._create_search_space(
进入对应区域:autosklearn.automl.AutoML#_create_search_space
看到configuration_space = pipeline.get_configuration_space(
进入对应区域:autosklearn.util.pipeline.get_configuration_space
在这个函数中,配置了info字典之后,最后一段代码:

    if info['task'] in REGRESSION_TASKS:
        return _get_regression_configuration_space(info, include, exclude)
    else:
        return _get_classification_configuration_space(info, include, exclude)
  • 进入对应区域:autosklearn.util.pipeline._get_classification_configuration_space

最后一段代码:

    return SimpleClassificationPipeline(
        dataset_properties=dataset_properties,
        include=include, exclude=exclude).\
        get_hyperparameter_search_space()
  • 进入对应区域:autosklearn.pipeline.base.BasePipeline#get_hyperparameter_search_space

最后一段代码:

        if not hasattr(self, 'config_space') or self.config_space is None:
            self.config_space = self._get_hyperparameter_search_space(
                include=self.include_, exclude=self.exclude_,
                dataset_properties=self.dataset_properties_)
        return self.config_space
  • 进入对应区域:autosklearn.pipeline.classification.SimpleClassificationPipeline#_get_hyperparameter_search_space

至此,进过多次跳转与入栈,我们终于进入了”干货“最为丰富的区域了。
看到如下代码:

        cs = self._get_base_search_space(
            cs=cs, dataset_properties=dataset_properties,
            exclude=exclude, include=include, pipeline=self.steps)

注意,这里的self.steps表示autosklearn想要优化出的Pipeline的所有节点。

  • 进入对应区域:autosklearn.pipeline.base.BasePipeline#_get_base_search_space

看到要获取matches,我们想知道matches是怎么来的:

  • 进入对应区域:autosklearn.pipeline.create_searchspace_util.get_match_array

for node_name, node in pipeline:这个循环中,构造了一个很重要的变量:node_i_choices,他是一个2维列表。在原生形式中,维度1为7,表示7个Pipeline的结点。其中每个子列表表示可以选择的所有option
我取前4个作为样例

node_i_choices[0]
Out[16]: 
[autosklearn.pipeline.components.data_preprocessing.one_hot_encoding.no_encoding.NoEncoding,
 autosklearn.pipeline.components.data_preprocessing.one_hot_encoding.one_hot_encoding.OneHotEncoder]
node_i_choices[1]
Out[17]: [Imputation(random_state=None, strategy='median')]
node_i_choices[2]
Out[18]: [VarianceThreshold(random_state=None)]
node_i_choices[3]
Out[19]: 
[autosklearn.pipeline.components.data_preprocessing.rescaling.minmax.MinMaxScalerComponent,
 autosklearn.pipeline.components.data_preprocessing.rescaling.none.NoRescalingComponent,
 autosklearn.pipeline.components.data_preprocessing.rescaling.normalize.NormalizerComponent,
 autosklearn.pipeline.components.data_preprocessing.rescaling.quantile_transformer.QuantileTransformerComponent,
 autosklearn.pipeline.components.data_preprocessing.rescaling.robust_scaler.RobustScalerComponent,
 autosklearn.pipeline.components.data_preprocessing.rescaling.standardize.StandardScalerComponent]

之后,matches_dimensions表示每个子列表的长度,用来构造一个高维张量matches

matches_dimensions
Out[20]: [2, 1, 1, 6, 1, 15, 15]
matches = np.ones(matches_dimensions, dtype=int)

看到:

    pipeline_idxs = [range(dim) for dim in matches_dimensions]
    for pipeline_instantiation_idxs in itertools.product(*pipeline_idxs):

可以理解为遍历这条Pipeline中所有的可能。
pipeline_instantiation_idxs表示某个Pipeline在matches中的坐标

pipeline_instantiation_idxs
Out[25]: (0, 0, 0, 0, 0, 0, 0)
            node_input = node.get_properties()['input']
            node_output = node.get_properties()['output']
node_input
Out[26]: (5, 6, 10)
node_output
Out[27]: (8,)

这个操作乍一看不理解,跳转get_properties函数我们看到:

                'input': (DENSE, SPARSE, UNSIGNED_DATA),
                'output': (PREDICTIONS,)}

应该是适应哪些类型。
首先判断sparse与dense是否check:

            # First check if these two instantiations of this node can work
            # together. Do this in multiple if statements to maintain
            # readability
            if (data_is_sparse and SPARSE not in node_input) or \
                    not data_is_sparse and DENSE not in node_input:
                matches[pipeline_instantiation_idxs] = 0
                break
            # No need to check if the node can handle SIGNED_DATA; this is
            # always assumed to be true
            elif not dataset_is_signed and UNSIGNED_DATA not in node_input:
                matches[pipeline_instantiation_idxs] = 0
                break

后面的操作也差不多,反正就是检查这个Pipeline是否合理。源码很sophisticated,我暂时跳过。
最后返回matches

  • 返回对应区域:autosklearn.pipeline.base.BasePipeline#_get_base_search_space:293
            if not is_choice:
                cs.add_configuration_space(node_name,
                                           node.get_hyperparameter_search_space(dataset_properties))
            # If the node isn't a choice, we have to figure out which of it's
            #  choices are actually legal choices
            else:
                choices_list = \
                    autosklearn.pipeline.create_searchspace_util.find_active_choices(
                        matches, node, node_idx,
                        dataset_properties,
                        include.get(node_name),
                        exclude.get(node_name)
                    )
                sub_config_space = node.get_hyperparameter_search_space(
                    dataset_properties, include=choices_list)
                cs.add_configuration_space(node_name, sub_config_space)

如果是选择性的结点,则进入else的部分,choices_list是所有的候选项

choices_list
Out[29]
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值