Ray Tune 搜索空间(Search Space)实战指南:网格搜索、随机采样与条件搜索空间
【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray
本文以 Ray Tune 的官方教程文档 tune-search-spaces.rst 为主体,系统讲解如何通过tune.Tuner(param_space=...)原生接口定义超参数搜索空间:既可以用tune.grid_search做确定性网格搜索,也可以用tune.uniform、tune.choice、tune.sample_from等随机采样原语描述概率分布,还可以借助sample_from的config参数构建"一个超参数取值依赖另一个超参数"的条件搜索空间。读完本文,你将掌握 Tune 搜索空间的全部核心原语、num_samples与网格组合的计数规则,以及搜索算法兼容性等实战注意事项。
一、搜索空间概述:从Tuner(param_space=...)开始
Ray Tune 提供了原生(native)的搜索空间描述接口。与旧式tune.run(config=...)不同,新式 API 统一通过Tuner构造器的param_space参数传入配置:
from ray import tune tuner = tune.Tuner( trainable, param_space={"bar": tune.grid_search([True, False])}) results = tuner.fit()param_space是一个普通 Python 字典(Dict[str, Any]),其取值可以是三类内容:
- 确定性网格:用
tune.grid_search([...])列出所有取值,Tune 会逐一评估每一种组合; - 随机分布:用
tune.choice、tune.uniform、tune.sample_from等原语声明概率分布,每次 trial 随机采样; - 常量:直接写普通值(如
"const": "hello"),这类键在每个 trial 中保持不变,可以与其他搜索原语混用。
从源码看,Tuner.__init__将param_space一路透传给内部的实验配置(见 tuner.py),而真正"解析并生成 trial 配置"的是变体生成器(variant generator),其入口为generate_variants()(见 variant_generator.py)。它会将param_space中的网格项、分布项与常量项分开处理:网格项做笛卡尔积组合,分布项按采样器逐个求值,最终产出每个 trial 的完整config字典。
二、tune.grid_search:确定性网格搜索
grid_search是 Tune 搜索空间中最基础的原语,其实现位于 variant_generator.py:
@PublicAPI(stability="beta") def grid_search(values: Iterable) -> Dict[str, Iterable]: return {"grid_search": values}它返回一个{"grid_search": values}形式的字典,Tune 的变体生成器据此识别出网格项。grid_search有两个关键语义:
- 保证采样:网格中的每个值都会被实际评估,不会像随机采样那样可能漏掉某个取值;
- 笛卡尔积组合:如果
param_space中有多个grid_search变量,它们会按组合乘积展开——"x": grid_search([1, 2, 3])与"y": grid_search(["a", "b", "c"])组合后共产生 3 × 3 = 9 种配置。
2.1 网格组合与num_samples的计数规则
文档中的一组示例清晰展示了num_samples与网格的组合计数规则:
# 4 种不同配置:num_samples=1 × 4 个网格值 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=1), param_space={"x": tune.grid_search([1, 2, 3, 4])}) tuner.fit() # 3 种不同配置 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=1), param_space={"x": tune.grid_search([1, 2, 3])}) tuner.fit() # 6 种不同配置:num_samples=2 会重复整个网格 2 次 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=2), param_space={"x": tune.grid_search([1, 2, 3])}) tuner.fit() # 9 种不同配置:3 × 3 网格 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=1), param_space={ "x": tune.grid_search([1, 2, 3]), "y": tune.grid_search(["a", "b", "c"])}) tuner.fit() # 18 种不同配置:3 × 3 网格重复 2 次 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=2), param_space={ "x": tune.grid_search([1, 2, 3]), "y": tune.grid_search(["a", "b", "c"])}) tuner.fit() # 45 种不同配置:3 × 3 网格重复 5 次 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=5), param_space={ "x": tune.grid_search([1, 2, 3]), "y": tune.grid_search(["a", "b", "c"])}) tuner.fit()这些计数的源码依据在 variant_generator.py 的_count_spec_samples()中:网格变量数grid_count是各网格项长度的乘积,总 trial 数 =num_samples × grid_count。也就是说,num_samples控制的是"整个网格重复多少次",而不是"每个网格值采样多少次"。
2.2 网格与随机采样混合
网格搜索与随机采样原语是**可互操作(inter-operable)**的,二者既可以独立使用,也可以在同一param_space中组合:
# 6 种不同配置:x 随机采样,y 走 3 值网格,重复 2 次 tuner = tune.Tuner(trainable, tune_config=tune.TuneConfig(num_samples=2), param_space={ "x": tune.sample_from(lambda _: np.random.uniform(100)), "y": tune.grid_search(["a", "b", "c"])}) tuner.fit()注意此时x的随机采样是针对每个网格组合分别重新采样的:网格的每种组合都会触发一次sample_from求值,从而产生不同随机取值。
三、随机采样原语:从均匀分布到自定义函数
除了grid_search,Tune 还提供了一整套随机采样原语。它们的实现集中在 sample.py,并在此文件的 docstring 与 search_space.rst 中给出了完整的语义说明。常用原语一览:
| 原语 | 签名 | 语义 | 源码位置 |
|---|---|---|---|
tune.uniform(lower, upper) | 浮点 | 在[lower, upper]上均匀采样,等价于np.random.uniform(lower, upper) | sample.py |
tune.quniform(lower, upper, q) | 浮点 | 均匀采样后量化到q的整数倍 | sample.py |
tune.loguniform(lower, upper) | 浮点 | 在对数空间均匀采样,适合跨越多个数量级的参数(如学习率 1e-4 ~ 1e-2) | sample.py |
tune.qloguniform(lower, upper, q) | 浮点 | 对数空间采样 + 量化 | sample.py |
tune.randn(mean=0.0, sd=1.0) | 浮点 | 正态分布采样 | sample.py |
tune.qrandn(mean, sd, q) | 浮点 | 正态分布采样 + 量化 | sample.py |
tune.randint(lower, upper) | 整数 | 在[lower, upper)上均匀采样(lower含、upper不含) | sample.py |
tune.qrandint(lower, upper, q) | 整数 | 整数均匀采样后量化(q=1时upper不含,其余情况含) | sample.py |
tune.lograndint(lower, upper) | 整数 | 对数空间整数采样 | sample.py |
tune.qlograndint(lower, upper, q) | 整数 | 对数空间整数采样 + 量化 | sample.py |
tune.choice(categories) | 类别 | 从候选列表中均匀挑选一个,等价于np.random.choice | sample.py |
tune.sample_from(func) | 任意 | 调用自定义函数生成取值,支持条件搜索空间 | sample.py |
3.1 一个完整的分布组合示例
参考 search_space.rst,一个覆盖全部常见分布的param_space长这样:
config = { # 在 -5.0 ~ -1.0 上均匀采样浮点数 "uniform": tune.uniform(-5, -1), # 在 3.2 ~ 5.4 上均匀采样,并四舍五入到 0.2 的倍数 "quniform": tune.quniform(3.2, 5.4, 0.2), # 在 0.0001 ~ 0.01 上于对数空间均匀采样 "loguniform": tune.loguniform(1e-4, 1e-2), # 对数空间采样,并量化到 0.00005 的倍数 "qloguniform": tune.qloguniform(1e-4, 1e-1, 5e-5), # 均值 10、标准差 2 的正态分布 "randn": tune.randn(10, 2), # 正态分布采样,量化到 0.2 的倍数 "qrandn": tune.qrandn(10, 2, 0.2), # 在 -9(含)~ 15(不含)上均匀采样整数 "randint": tune.randint(-9, 15), # 在 -21 ~ 12 之间采样 3 的倍数(含 12) "qrandint": tune.qrandint(-21, 12, 3), # 在 1(含)~ 10(不含)上于对数空间采样整数 "lograndint": tune.lograndint(1, 10), # 对数空间整数采样,量化到 2 的倍数 "qlograndint": tune.qlograndint(1, 10, 2), # 从 ["a", "b", "c"] 中等概率选一个 "choice": tune.choice(["a", "b", "c"]), # 自定义函数采样,可引用本搜索空间中的其他键 "func": tune.sample_from(lambda config: config["uniform"] * 0.01), # 网格搜索,每个值会被采样 num_samples 次 "grid": tune.grid_search([32, 64, 128]), }其中量化类原语(quniform、qrandint等)的底层实现通过Domain.quantized(q)完成:采样后按round(value / q)就近取整,因此量化会把上界变为包含(详见 sample.py 中Float/Integer域的quantized逻辑)。这些分布原语本质上都返回一个Domain对象(Float、Integer、Categorical),它们携带取值边界与采样器(Uniform、LogUniform、Normal、Grid等),由Domain.sample()统一驱动采样。
四、num_samples的完整语义
num_samples是TuneConfig的核心字段,其源码定义见 tune_config.py:
@dataclass class TuneConfig: ... num_samples: int = 1官方注释明确其语义为:
Number of times to sample from the hyperparameter space. Defaults to 1. If
grid_searchis provided as an argument, the grid will be repeatednum_samplestimes. If this is -1, (virtually) infinite samples are generated until a stopping condition is met.
即:
- 默认值为 1;
- 若搜索空间中含
grid_search,则同一个网格会重复num_samples次(网格组合数 ×num_samples= 总 trial 数); - 设为-1时(近乎)无限采样,直到满足停止条件(如
time_budget_s时间预算或自定义 Stopper); - 可搭配
time_budget_s、max_concurrent_trials(最大并发 trial 数,通过ConcurrencyLimiter包装搜索算法实现)等字段一起使用。
带num_samples的完整示例(来自原文档,其中网格为 3×3,重复 10 次,共 90 个 trial):
tuner = tune.Tuner( my_trainable, run_config=tune.RunConfig(name="my_trainable"), # num_samples 会把整个配置重复 10 次 tune_config=tune.TuneConfig(num_samples=10), param_space={ # ``sample_from`` 创建一个生成器,每个 trial 调用一次 lambda "alpha": tune.sample_from(lambda _: np.random.uniform(100)), # ``sample_from`` 也支持"条件搜索空间" "beta": tune.sample_from(lambda config: config["alpha"] * np.random.normal()), "nn_layers": [ # tune.grid_search 会保证所有取值都被评估 tune.grid_search([16, 64, 256]), tune.grid_search([16, 64, 256]), ], }, ) tuner.fit()这里nn_layers是一个列表,其中两项都是grid_search([16, 64, 256]),网格组合数为 3 × 3 = 9;配合num_samples=10,总共生成 90 个 trial,每个 trial 中alpha、beta都会被重新随机采样。这正体现了前文所说的核心规则:网格被整体重复num_samples次,而随机分布在每次重复中重新采样。
五、自定义与条件搜索空间:tune.sample_from
现实中的超参数搜索常常遇到"awkward search spaces"——即某个超参数的最优取值范围依赖于另一个超参数。例如隐藏层宽度取决于特征维度、dropout 范围取决于网络深度等。此时应使用tune.sample_from(func)提供自定义可调用函数来生成取值。
sample_from的源码实现非常简单(见 sample.py):
@PublicAPI def sample_from(func: Callable[[Dict], Any]): """Specify that tune should sample configuration values from this function. ...""" return Function(func)它返回一个Function域对象。关键在于func的约定:它接收一个config字典,其中包含该 trial 已经采样好的其他超参数取值。正因为可以读取config中的其他键,sample_from成为构建条件分布(conditional distributions)的利器。
5.1 基础条件采样
tuner = tune.Tuner( ..., param_space={ # 一个随机函数 "alpha": tune.sample_from(lambda _: np.random.uniform(100)), # 利用 config 字典访问其他超参数 "beta": tune.sample_from(lambda config: config["alpha"] * np.random.normal()) } ) tuner.fit()这里beta的取值 =alpha× 一个标准正态噪声,beta的分布完全依赖于alpha的采样结果。lambda 中通过config["alpha"]引用另一个键,从而表达"参数间依赖"。
5.2 网格 + 条件采样的完整组合
原文档给出了一个综合示例:两个嵌套参数的网格搜索 + 两个 lambda 的随机采样,共生成 9 个不同 trial(3 × 3 网格),且beta的值依赖alpha:
tuner = tune.Tuner( my_trainable, run_config=RunConfig(name="my_trainable"), param_space={ "alpha": tune.sample_from(lambda _: np.random.uniform(100)), "beta": tune.sample_from(lambda config: config["alpha"] * np.random.normal()), "nn_layers": [ tune.grid_search([16, 64, 256]), tune.grid_search([16, 64, 256]), ], } )nn_layers为两元素列表(对应两层网络各 16/64/256 三种宽度),网格展开后每个 trial 的alpha、beta都重新采样。这种"网格确定结构 + lambda 随机填值"的混合写法是 Tune 中最常用的搜索空间组织方式。
六、使用注意事项
6.1 搜索算法兼容性
原文档开篇即给出重要警告(caution):
If you use a SearchAlgorithm, you may not be able to specify lambdas or grid search with this interface, as some search algorithms may not be compatible.
也就是说,一旦为TuneConfig指定了外部搜索算法(SearchAlgorithm,如贝叶斯优化、进化算法等),sample_from的 lambda 和grid_search可能不被支持,因为这类算法要求把搜索空间转换成自己定义的受限结构。同样地,search_space.rst 也强调:"Not all Search Algorithms support all distributions. In particular,tune.sample_fromandtune.grid_searchare often unsupported. The default BasicVariantGenerator supports all distributions."
默认的BasicVariantGenerator(即不指定search_alg时的默认变体生成器)支持全部搜索空间原语。若你依赖默认行为,无需担心兼容性问题。
6.2 条件搜索空间:仅部分算法支持
原文档进一步说明:
This format is not supported by every SearchAlgorithm, and only some SearchAlgorithms, like HyperOpt and Optuna, handle conditional search spaces at all.
- HyperOpt:要使用条件搜索空间,需要借助 Hyperopt 自身的搜索空间 DSL(
hp.choice、hp.pchoice等定义条件分支的写法); - Optuna:支持通过其 define-by-run 接口表达条件搜索空间(在
objective函数内动态定义trial.suggest_*,配合Tuner的param_space使用)。
因此,如果你的调优方案依赖条件搜索空间,务必确认所选搜索算法是否原生支持;否则应回退到默认变体生成器。
6.3 性能:避免在搜索空间中传递大对象
原文档给出 tip:
Avoid passing large objects as values in the search space, as that will incur a performance overhead.
不要把大型对象(如整个数据集、大模型权重、预训练词表等)直接作为param_space的值,否则每次 trial 生成与序列化都会产生显著开销。推荐做法:
- 使用
tune.with_parameters将大对象作为不可变参数随 trainable 传递; - 或者让 trainable 从磁盘 / 云存储加载大对象(需保证所有节点都能访问相应文件)。
6.4 关于文档示例中randn的说明
原文档演示num_samples时写有"y": tune.randn([0, 1, 2])的示例。需要指出,tune.randn的真实签名为randn(mean: float = 0.0, sd: float = 1.0)(见 sample.py),它返回一个正态分布域而非取值列表;若要表达离散候选,应使用tune.choice或tune.grid_search。该示例属于文档中的示意性写法,实际使用时请以本节给出的签名为准。
七、小结
Ray Tune 的搜索空间体系可以用一条主线概括:Tuner(param_space=...)是唯一入口,grid_search与随机分布原语是两类基本构件,num_samples决定重复次数,sample_from提供表达参数依赖的终极灵活性。
| 需求 | 推荐写法 |
|---|---|
| 穷举少量离散候选 | tune.grid_search([...]) |
| 连续区间均匀采样 | tune.uniform(lower, upper) |
| 跨数量级采样(学习率等) | tune.loguniform(1e-4, 1e-2) |
| 离散类别随机选择 | tune.choice([...]) |
| 整数范围采样 | tune.randint(lower, upper) |
| 参数间存在依赖 | tune.sample_from(lambda config: ...) |
| 控制总 trial 数 | TuneConfig(num_samples=N),网格重复 N 次 |
理解num_samples与网格的笛卡尔积组合规则(总 trial 数 = 网格组合数 ×num_samples),并牢记"lambda/grid 与部分搜索算法不兼容""避免大对象入搜索空间"两条经验,你就能在设计超参数调优实验时精确掌控 trial 数量与搜索策略。相关代码与文档可继续参阅 tune-search-spaces.rst、search_space.rst、sample.py 与 variant_generator.py。
【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考