{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 04. 详解优化器"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 实例化优化器对象"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在Basic Tutorial中,我们知道UltraOpt中有如下优化器:"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"|优化器|描述|\n",
"|-----|---|\n",
"|ETPE| Embedding-Tree-Parzen-Estimator, 是UltraOpt作者自创的一种优化算法,在TPE算法[[4]](#refer-anchor-4)的基础上对类别变量采用Embedding降维为低维连续变量,
并在其他的一些方面也做了改进。ETPE在某些场景下表现比HyperOpt的TPE算法要好。 |\n",
"|Forest |基于随机森林的贝叶斯优化算法。概率模型引用了`scikit-optimize`[[1]](#refer-anchor-1)包的`skopt.learning.forest`模型[[2]](#refer-anchor-2),
并借鉴了`SMAC3`[[3]](#refer-anchor-3)中的局部搜索方法|\n",
"|GBRT| 基于梯度提升回归树(Gradient Boosting Resgression Tree)的贝叶斯优化算法,
概率模型引用了`scikit-optimize`包的`skopt.learning.gbrt`模型 |\n",
"|Random| 随机搜索。 |"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"我们在调用`ultraopt.fmin`函数进行优化时,在使用优化器默认参数的情况下,可以只传入优化器的名字,如:\n",
"\n",
"```python\n",
"from ultraopt import fmin\n",
"\n",
"result = fmin(evaluate_function, config_space, optimizer=\"ETPE\")\n",
"```\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"但如果我们要调整优化器的参数,如修改`ForestOptimizer`随机森林树的个数,或 `ETPEOptimizer` 的一些涉及冷启动和采样个数的参数时,就需要从`ultraopt.optimizer`包中引入相应的优化类,并对其实例化为优化器对象"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"引入`fmin`和你需要使用的优化器类:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from ultraopt import fmin\n",
"from ultraopt.optimizer import ETPEOptimizer\n",
"from ultraopt.optimizer import RandomOptimizer\n",
"from ultraopt.optimizer import ForestOptimizer\n",
"from ultraopt.optimizer import GBRTOptimizer"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"引入一个测试用的配置空间和评价函数:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from ultraopt.tests.mock import config_space, evaluate"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"根据自己想要的参数实例化一个`ForestOptimizer`:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"optimizer = ForestOptimizer(n_estimators=20) # 代理模型为20棵树的随机森林"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"把优化器对象传入fmin函数,开始优化过程"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"100%|██████████| 100/100 [00:04<00:00, 20.13trial/s, best loss: 2.505]\n"
]
},
{
"data": {
"text/plain": [
"+---------------------------------+\n",
"| HyperParameters | Optimal Value |\n",
"+-----------------+---------------+\n",
"| x0 | -0.5365 |\n",
"| x1 | 0.3258 |\n",
"+-----------------+---------------+\n",
"| Optimal Loss | 2.5050 |\n",
"+-----------------+---------------+\n",
"| Num Configs | 100 |\n",
"+-----------------+---------------+"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"fmin(evaluate, config_space, optimizer)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 不用fmin函数自己实现一个优化过程"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"优化器有3个重要的函数:\n",
"\n",
"- `initialize(config_space, ...` : 参数为配置空间等,初始化优化器\n",
"- `ask(n_points=None , ...` : 请求优化器推荐`n_points`个配置(默认为1个)\n",
"- `tell(config, loss, ...)` : 告知优化器,之前推荐配置`config`的好坏(损失`loss`越小越好)\n",
"\n",
"优化器的运行流程如下图所示:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from graphviz import Digraph; g = Digraph()\n",
"g.node(\"config space\", shape=\"ellipse\"); g.node(\"optimizer\", shape=\"box\")\n",
"g.node(\"config\", shape=\"ellipse\"); g.node(\"loss\", shape=\"circle\"); g.node(\"evaluator\", shape=\"box\")\n",
"g.edge(\"config space\", \"optimizer\", label=\"initialize\"); g.edge(\"optimizer\", \"config\", label=\"<ask>\", color='blue')\n",
"g.edge(\"config\",\"evaluator\" , label=\"send to\"); g.edge(\"evaluator\",\"loss\" , label=\"evaluate\")\n",
"g.edge(\"config\", \"optimizer\", label=\"<tell>\", color='red'); g.edge(\"loss\", \"optimizer\", label=\"<tell>\", color='red')\n",
"g.graph_attr['rankdir'] = 'LR'; g"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"图中的`evaluator`评价器会对`config`配置进行评价,然后返回一个损失`loss`。\n",
"\n",
"举个例子,在AutoML问题中,评价器的工作流程如下:\n",
"1. 将config转化为一个机器学习实例\n",
"2. 在训练集上对机器学习实例进行训练\n",
"3. 在验证集上得到相应的评价指标\n",
"4. 对评价指标进行处理,使其`越小越好`,返回`loss`\n",
"\n",
"具体的评价器我们会在下个教程中实现。在学习了这些知识后, 我们能否脱离`fmin`函数,自己实现一个优化过程呢?答案是可以的。\n",
"\n",
"在UltraOpt的设计哲学中,优化器只需要具备上述的3个接口,评价器和分布式策略的设计都可以由用户完成。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 先从一次循环中体会整个过程\n",
"\n",
"> Step 1. 首先实例化和初始化优化器"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"optimizer = ETPEOptimizer()\n",
"optimizer.initialize(config_space)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Step 2. 调用优化器的`ask`函数获取一个其推荐的配置:"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'x0': 6.632914250425159, 'x1': 1.830923578719112}"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"recommend_config, config_info = optimizer.ask()\n",
"recommend_config"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'model_based_pick': False, 'origin': 'Initial Design'}"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"config_info"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Step 3. 用评估器,在这是`evaluate`函数来评价配置的好坏"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"177817.31410476987"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"loss = evaluate(recommend_config)\n",
"loss"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> Step 4. 通过tell函数将观测结果 `config, loss` 传递给优化器"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"optimizer.tell(recommend_config, loss)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### 将上述过程整理为一个for循环"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"optimizer = ETPEOptimizer()\n",
"optimizer.initialize(config_space)\n",
"losses = []\n",
"best_losses = []\n",
"for _ in range(100):\n",
" config, _ = optimizer.ask()\n",
" loss = evaluate(config)\n",
" optimizer.tell(config, loss)\n",
" losses.append(loss)\n",
" best_losses.append(min(losses))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"import pylab as plt"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"