auto-sklearn简析


  这是笔者在中国科学院大学上王伟老师的面向对象程序设计一课时的作业要求——对auto-sklearn进行分析以考察面向对象思想在其中的应用。
  因此,本文的重点不是auto-sklearn的介绍与分析,也不是具体源码实现的解析与算法设计的讨论,而是对需求建模、主要功能流程设计、类及类间关系、面向对象设计原则及设计模式的分析。总而言之,是对面向对象思想的探讨。

一. auto-sklearn简介

0. 什么是sklearn

  但凡接触过机器学习的人对sklearn(scikit-learn)一定不陌生,它是基于Python语言的机器学习工具,是机器学习中常用的第三方模块,它对常用的机器学习方法进行了封装,包括回归、降维、分类、聚类等方法。
在这里插入图片描述
  sklearn具有广泛的应用,其功能非常强大,但此处不会做过多展开,如果读者希望进一步了解sklearn,这里提供sklearn的中文官网供参考:Introduction·sklearn

1. 什么是auto-sklearn

  auto-sklearn 提供了开箱即用的监督型自动机器学习。从名字可以看出,auto-sklearn 是基于机器学习库 scikit-learn 构建的,可为新的数据集自动搜索学习算法,并优化其超参数。因此,它将机器学习使用者从繁琐的任务中解放出来,使其有更多时间专注于实际问题。当前版本为 0.6.0,具体信息请查看官网auto-sklearn

2. auto-sklearn可以auto到什么程度?

在这里插入图片描述
  我们以机器学习的分类模型为例,常规的机器学习框架如图中的灰色部分,导入数据后,在经过数据预处理和特征预处理后,通过分类器输出预测值,如果结果不尽人意,需要手动调整超参数并重新选择合适的模型。
  而自动的部分就如图绿框所示,在ML-framework左边新增meta-learning,在右边新增build-ensemble,并使用贝叶斯优化自动调超参数。meta-learning是用于初始化贝叶斯优化器的元学习,它可以去学习样本数据的模样,一旦找到相似的数据集,就可以根据经验来推荐好用的分类器,比如文本数据用什么模型比较好,很多离散的数据用什么模型比较好。build-ensemble是优化过程中的自动模型集成,可以根据贝叶斯优化找到最佳的分类器组合,往往能提高预测的准确性。

3. auto-sklearn有什么特点

(1)优点

  • 支持设置单次训练时间和总体训练时间,使得工具既能限制训练时间,又能充分利用时间和算力
  • 支持切分训练/测试集的方式,也支持使用交叉验证,从而减少了训练模型的代码量和程序的复杂程度
  • 支持加入扩展模型以及扩展预测处理方法,使训练具有多样性和灵活性

(2)缺点

  • 输出携带的信息较少,想进一步训练只能重写代码
  • 不支持深度学习
  • 适用范围有限,只适用于tabular data上的监督学习,对非数值型的数据不太友好

4. 怎么使用auto-sklearn

  对于给定的数据集,我们如何利用auto-sklearn来对它进行自动化的机器学习呢?上手的方法非常简单,设置好以下几种关键的参数即可。
在这里插入图片描述

  • 训练时间和内存的使用量的设置,其中时间包括所有模型训练时间的总和以及单个模型训练的最长时间,单位是秒;
  • 训练后模型的存储,参数默认为训练完成后删除训练的暂存目录和输出目录,使用以下参数,可指定其暂存目录及是否删除;
  • 数据的切分,使用resampling_strategy参数可设置训练集与测试集的切分方法,这里展示了设置五折交叉验证的方法;
  • 模型的选择,可以通过这个参数支持指定备选的机器学习模型,或者从所有模型中去掉一些机器学习模型。

以下是两个简单示例:

(1)回归任务

# -*- encoding: utf-8 -*-
"""
==========
Regression
==========

The following example shows how to fit a simple regression model with
*auto-sklearn*.
"""
import sklearn.datasets
import sklearn.metrics

import autosklearn.regression


############################################################################
# Data Loading
# ============

X, y = sklearn.datasets.load_boston(return_X_y=True)

X_train, X_test, y_train, y_test = \
    sklearn.model_selection.train_test_split(X, y, random_state=1)

############################################################################
# Build and fit a regressor
# =========================

automl = autosklearn.regression.AutoSklearnRegressor(
    time_left_for_this_task=120,
    per_run_time_limit=30,
    tmp_folder='/tmp/autosklearn_regression_example_tmp',
    output_folder='/tmp/autosklearn_regression_example_out',
)
automl.fit(X_train, y_train, dataset_name='boston')

############################################################################
# Print the final ensemble constructed by auto-sklearn
# ====================================================

print(automl.show_models())

###########################################################################
# Get the Score of the final ensemble
# ===================================

predictions = automl.predict(X_test)
print("R2 score:", sklearn.metrics.r2_score(y_test, predictions))

(2)分类任务

# -*- encoding: utf-8 -*-
"""
==============
Classification
==============

The following example shows how to fit a simple classification model with
*auto-sklearn*.
"""
import sklearn.datasets
import sklearn.metrics

import autosklearn.classification


############################################################################
# Data Loading
# ============

X, y = sklearn.datasets.load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = \
    sklearn.model_selection.train_test_split(X, y, random_state=1)

############################################################################
# Build and fit a regressor
# =========================

automl = autosklearn.classification.AutoSklearnClassifier(
    time_left_for_this_task=120,
    per_run_time_limit=30,
    tmp_folder='/tmp/autosklearn_classification_example_tmp',
    output_folder='/tmp/autosklearn_classification_example_out',
)
automl.fit(X_train, y_train, dataset_name='breast_cancer')

############################################################################
# Print the final ensemble constructed by auto-sklearn
# ====================================================

print(automl.show_models())

###########################################################################
# Get the Score of the final ensemble
# ===================================

predictions = automl.predict(X_test)
print("Accuracy score:", sklearn.metrics.accuracy_score(y_test, predictions))

二. 主要功能分析与建模

1. 从一个简单的示例出发

以下是官网给出一个最简单的多分类示例:

"""
==========================
Multi-label Classification
==========================

This examples shows how to format the targets for a multilabel classification
problem. Details on multilabel classification can be found
`here <https://scikit-learn.org/stable/modules/multiclass.html>`_.
"""
import numpy as np

import sklearn.datasets
import sklearn.metrics
from sklearn.utils.multiclass import type_of_target

import autosklearn.classification


############################################################################
# Data Loading
# ============

# Using reuters multilabel dataset -- https://www.openml.org/d/40594
X, y = sklearn.datasets.fetch_openml(data_id=40594, return_X_y=True, as_frame=False)

# fetch openml downloads a numpy array with TRUE/FALSE strings. Re-map it to
# integer dtype with ones and zeros
# This is to comply with Scikit-learn requirement:
# "Positive classes are indicated with 1 and negative classes with 0 or -1."
# More information on: https://scikit-learn.org/stable/modules/multiclass.html
y[y == 'TRUE'] = 1
y[y == 'FALSE'] = 0
y = y.astype(np.int)

# Using type of target is a good way to make sure your data
# is properly formatted
print(f"type_of_target={type_of_target(y)}")

X_train, X_test, y_train, y_test = sklearn.model_selection.train_test_split(
    X, y, random_state=1
)

############################################################################
# Building the classifier
# =======================

automl = autosklearn.classification.AutoSklearnClassifier(
    time_left_for_this_task=60,
    per_run_time_limit=30,
    # Bellow two flags are provided to speed up calculations
    # Not recommended for a real implementation
    initial_configurations_via_metalearning=0,
    smac_scenario_args={'runcount_limit': 1},
)
automl.fit(X_train, y_train, dataset_name='reuters')

############################################################################
# Print the final ensemble constructed by auto-sklearn
# ====================================================

print(automl.show_models())

############################################################################
# Print statistics about the auto-sklearn run
# ===========================================

# Print statistics about the auto-sklearn run such as number of
# iterations, number of models failed with a time out.
print(automl.sprint_statistics())

############################################################################
# Get the Score of the final ensemble
# ===================================

predictions = automl.predict(X_test)
print("Accuracy score", sklearn.metrics.accuracy_score(y_test, predictions))

  实现过程注释里面给的很清晰,分为了几个部分,数据加载(data loading),创建分类器(build the classifier),得出结果(get the score),而其中的核心过程创建分类器用到的方法基本来自于对象automl,包括automl.fit()automl.show_models()automl.sprint_statistics()automl.predict(),而这个对象在这里被创建为类AutoSklearnClassifier的一个实例。
  正如我们一开始就讲到,文章的重点是面向对象思想,而非项目本身。所以我们试图通过选择简单的分析样例来避开项目自身的实现复杂性,避开诸多细节,只接触核心流程。

2. 需求建模

  首先,我们对上述示例进行需求建模:

【用例名称】
多分类自动化机器学习
【场景】
who:训练集、测试集、分类模型、预测结果
where:内存空间
when:运行时间
【用例描述】

  1. 载入数据集
  2. 进行数据预处理
    2.1 若数据集没有划分训练集和测试集,按一定规则划分
    2.2 若有归一化需求,则将数据进行归一化
  3. 创建合适的分类器
  4. 进行训练,自动化调参
  5. 打印最终模型及其相关参数
  6. 对测试集数据进行分类预测,得到预测结果的正确率

【用例价值】
完成多分类的机器学习任务
【约束和限制】
输入数据为数值型

  寻找其中的动词和名词:

【动词】载入、预处理、划分、归一化、创建、训练、预测、打印模型
【名词】数据集、训练集、测试集、分类器、预测结果

  数据集(包括训练集和测试集)为一系列数值,没有必要抽象成类,从而我们得到应该抽象出来的类及其方法和属性:

【类】分类器(AutoSklearnClassifier)
【属性】训练需要的相关信息,包括训练时间,模型存储等
【方法】训练、预测、打印模型

  而这与之前得到的对象automl的性质正好相对应起来了。

3. 实际执行流程

  1. 完成自动化机器学习任务的最核心过程就是模型的训练过程了,在代码中体现为对automl对象的fit()方法的调用,其具体执行流程如下:
  fit()方法是AutoSklearnClassifier这个类下声明的第一个方法:

class AutoSklearnClassifier(AutoSklearnEstimator):

    def fit(self, X, y,
            X_test=None,
            y_test=None,
            feat_type=None,
            dataset_name=None):
        """Fit *auto-sklearn* to given training set (X, y).

        Fit both optimizes the machine learning models and builds an ensemble
        out of them. To disable ensembling, set ``ensemble_size==0``.

        """
        # Before running anything else, first check that the
        # type of data is compatible with auto-sklearn. Legal target
        # types are: binary, multiclass, multilabel-indicator.
        target_type = type_of_target(y)
        supported_types = ['binary', 'multiclass', 'multilabel-indicator']
        if target_type not in supported_types:
            raise ValueError("Classification with data of type {} is "
                             "not supported. Supported types are {}. "
                             "You can find more information about scikit-learn "
                             "data types in: "
                             "https://scikit-learn.org/stable/modules/multiclass.html"
                             "".format(
                                    target_type,
                                    supported_types
                                )
                             )

        # remember target type for using in predict_proba later.
        self.target_type = target_type

        super().fit(
            X=X,
            y=y,
            X_test=X_test,
            y_test=y_test,
            feat_type=feat_type,
            dataset_name=dataset_name,
        )

        return self
	......

  2. 在这个方法中,通过调用super().fit()实现函数主体过程,而在python语法中,与java类似的,使用super关键字表示调用父类的方法,即此处通过调用父类的fit()方法来实现该类(指AutoSklearnClassifier)的相同方法,相当于该类继承使用其父类的同名方法。
  其父类的fit()方法定义如下:

def fit(self, **kwargs):

        # Handle the number of jobs and the time for them
        if self.n_jobs is None or self.n_jobs == 1:
            self._n_jobs = 1
        elif self.n_jobs == -1:
            self._n_jobs = joblib.cpu_count()
        else:
            self._n_jobs = self.n_jobs

        # Automatically set the cutoff time per task
        if self.per_run_time_limit is None:
            self.per_run_time_limit = self._n_jobs * self.time_left_for_this_task // 10

        seed = self.seed
        self.automl_ = self.build_automl(
            seed=seed,
            ensemble_size=self.ensemble_size,
            initial_configurations_via_metalearning=(
                self.initial_configurations_via_metalearning
            ),
            tmp_folder=self.tmp_folder,
            output_folder=self.output_folder,
        )
        self.automl_.fit(load_models=self._load_models, **kwargs)

        return self

  3. 在这个过程中,首先调用了该类(指AutoSklearnClassifier的父类)的build_automl()方法创建了一个自动化机器学习器,该方法代码如下:

def build_automl(
        self,
        seed: int,
        ensemble_size: int,
        initial_configurations_via_metalearning: int,
        tmp_folder: str,
        output_folder: str,
        smac_scenario_args: Optional[Dict] = None,
    ):

        backend = create(
            temporary_directory=tmp_folder,
            output_directory=output_folder,
            delete_tmp_folder_after_terminate=self.delete_tmp_folder_after_terminate,
            delete_output_folder_after_terminate=self.delete_output_folder_after_terminate,
            )

        if smac_scenario_args is None:
            smac_scenario_args = self.smac_scenario_args

        automl = self._get_automl_class()(
            backend=backend,
            time_left_for_this_task=self.time_left_for_this_task,
            per_run_time_limit=self.per_run_time_limit,
            initial_configurations_via_metalearning=initial_configurations_via_metalearning,
            ensemble_size=ensemble_size,
            ensemble_nbest=self.ensemble_nbest,
            max_models_on_disc=self.max_models_on_disc,
            seed=seed,
            memory_limit=self.memory_limit,
            include_estimators=self.include_estimators,
            exclude_estimators=self.exclude_estimators,
            include_preprocessors=self.include_preprocessors,
            exclude_preprocessors=self.exclude_preprocessors,
            resampling_strategy=self.resampling_strategy,
            resampling_strategy_arguments=self.resampling_strategy_arguments,
            n_jobs=self._n_jobs,
            dask_client=self.dask_client,
            get_smac_object_callback=self.get_smac_object_callback,
            disable_evaluator_output=self.disable_evaluator_output,
            smac_scenario_args=smac_scenario_args,
            logging_config=self.logging_config,
            metadata_directory=self.metadata_directory,
            metric=self._metric,
            scoring_functions=self._scoring_functions
        )

        return automl

  可见其通过调用_get_automl_class()方法创建了AutoML类的一个实例,即上一段提到的自动化机器学习器(对应代码中的self.automl_对象)

  4. 在build_automl()创建了self.automl_对象之后,调用了self.automl_.fit(),即类AutoMLfit()方法,完成学习和训练过程。
  可见这个用户表面上调用的fit()函数实际上经过了三层不同的类的fit()方法的迭代,最终运行的是类AutoMLfit()方法,而这个最终版的fit()方法代码量较大,在这里就不再展示,感兴趣的读者可以直接进入官方github网站的automl.py文件进行更深入的研究。
  实际上,这也是我觉得该项目的设计中略不合理的地方,一个如此长的函数给代码的维护和阅读带来了及其不良的体验,也会带来大量代码的高度耦合,这并不是良好的程序设计风格。就面向对象程序设计而言,这也不符合封装和模块化的思想以及高内聚、低耦合的要求。

三. 核心流程设计分析

1. 类间关系

  在上一节的分析中,我们提及了AutoSklearnClassifier及其父类,还有AutoML这三个类,它们以及其他相关类的类间关系如下图:

AutoSklearnEstimator automl_ : NoneType time_left_for_this_task : int per_run_time_limit : NoneType tmp_folder : NoneType output_folder : NoneType n_jobs : NoneType ...... build_automl() fit() predict(X, batch_size, n_jobs) refit(X, y) score(X, y) show_models() sprint_statistics() AutoSklearnClassifier target_type fit(X, y, X_test, y_test, feat_type, dataset_name) predict(X, batch_size, n_jobs) AutoSklearnRegressor fit(X, y, X_test, y_test, feat_type, dataset_name) predict(X, batch_size, n_jobs) AutoML models_ : NoneType, list precision : int fit(X, y, task, X_test, y_test, feat_type, dataset_name) get_models_with_weights() predict(X, batch_size, n_jobs) refit(X, y) score(X, y) show_models() sprint_statistics() AutoMLClassifier fit(X, y, X_test, y_test, feat_type, dataset_name) predict(X, batch_size, n_jobs) AutoMLRegressor fit(X, y, X_test, y_test, feat_type, dataset_name)

  与分类器相类似的,机器学习任务中另一种非常重要的模型是回归器(regressor),因此回归器将作为与分类器相并列的一个类AutoSklearnRegressor而存在,它们的父类AutoSklearnEstimator与类AutoML相关联,从而能够在其自身的fit()方法中调用类AutoMLfit()方法。
  类AutoML下也实现了子类AutoMLClassifierAutoMLRegressor,通过vscode工具查看引用发现这两个类下的fit()方法除了其自身没有被其他任何地方引用,这让我有些纳闷。
在这里插入图片描述
  事实上,这两个子类的fit()方法也是通过super关键字调用super().fit()实现对类AutoMLfit()方法的最终调用的,因此这两个类的功能与之前提到的AutoSklearnRegressorAutoSklearnClassifier两个类相比确实有一些重复,所以这也是我认为不太合理的地方,不知道是不是开发者为了提高代码的兼容性而故意保留的冗余部分。

2. 时序图

  根据之前的分析可以画出执行流程的时序图如下:
在这里插入图片描述

四. 高级设计意图分析

1. 策略模式

  策略模式的目的是,针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,在auto-sklearn或sklearn这样的机器学习库中,策略模式的应用是非常常见且典型的。
  比如,对于机器学习中的几个关键过程,分类(classification),回归(regression),数据预处理(data-preprocessing),和特征预处理(feature-preprocessing),每一个过程都有很多种不同的算法或方法来实现,如分类模型有adaboost、decision_tree、k_nearest_neighbors等等,在这个项目中,每一种算法被封装成一个独立的类,这些类均继承于一个与它们具有相同功能的抽象的类,这个类作为所有支持的算法的公共接口。components部分的base模块生成的类图如下:
在这里插入图片描述
  其中AutoSklearnClassificationAlgorithmAutoSklearnPreprocessingAlgorithmAutoSklearnRegressionAlgorithm即为几个公共接口(Strategy),Context使用这个接口来调用某ConcreteStrategy定义的算法,而ConcreteStrategy即为各种具体的算法。包括了ConcreteStrategy部分的简略类图如下(为降低复杂性略去了类IterativeComponent和类IterativeComponentWithSampleWeight,且由于算法种类过多只展示部分算法,重点是表达设计思想):

AutoSklearnComponents «interface» AutoSklearnClassificationAlgorithm «interface» AutoSklearnPreprocessingAlgorithm «interface» AutoSklearnRegressionAlgorithm BaseEstimator AdaboostClassifier BernoulliNB DecisionTree GaussianNB KNearestNeighborsClassifier
AutoSklearnComponents «interface» AutoSklearnClassificationAlgorithm «interface» AutoSklearnPreprocessingAlgorithm «interface» AutoSklearnRegressionAlgorithm BaseEstimator PCA RandomTreesEmbedding SelectClassificationRates LibLinear_Preprocessor
AutoSklearnComponents «interface» AutoSklearnClassificationAlgorithm «interface» AutoSklearnPreprocessingAlgorithm «interface» AutoSklearnRegressionAlgorithm BaseEstimator ARDRegression KNearestNeighborsRegressor GaussianProcess LibSVM_SVR LibLinear_SVR

  通过策略模式使得架构清晰且符合开闭原则,我们可以很容易增加新的算法或修改原有算法。与此同时,该项目还很好地利用了其他项目的策略模式,从图中可以看到有一个新的类BaseEstimator,这个类与类AutoSklearnComponents的定义相关,但是通过查询它并不存在于本项目的开源代码中,通过vscode检索工具最终发现它出现在了sklearn源码的base模块,如果把类BaseEstimator也看作sklearn的一个接口,那么类AutoSklearnComponents相当于对sklearn中预测器(类BaseEstimator)的一种实现。

2. 代理模式

  代理模式的意图为为其他对象提供一种代理以控制对这个对象的访问,其典型的实现方法就是增加中间层,使得代理可以代替实体实现相关功能。
  通过前几节的分析,可以发现auto-sklearn的核心设计模式就是代理模式。最终要访问的实体类是AutoML,而类AutoSklearnEstimator相当于它的代理,这个类中并没有fit()方法真正的具体实现,但通过调用它我们却可以得到我们想要的功能和结果,这或许与传统意义上的代理模式略有不同,但其核心思想是相通的。创建一个类AutoML的实例需要比较大的开销,所以这里先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。 这应该属于虚拟代理。

五. 结语

  所有的分析到这里就结束了,作为一个面向对象编程领域的小白,一个学期下来收获很多,在报告中提出了一些自己不成熟的见解,感谢各位读者的耐心阅读和体谅,有机会也希望与大家共同交流学习~

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值