1. 搜索空间概念与基本定义归纳总结
好,咱们正式开始。第一章,先把地基打牢。
搜索空间,说白了就是你所有可能解构成的集合。你做参数调优、做路径规划、做神经网络架构搜索,本质上都是在某个空间里翻来覆去地找那个最优解。
我刚开始搞搜索空间优化时,犯过一个低级错误:把空间设得太大。一组参数从 -100 到 +100,步长 0.001,你想想看,那组合数量多大?结果搜了三天三夜,还在原地打转。所以,理解搜索空间的定义和边界,是第一步,也是最重要的一步。
1.1 到底什么是搜索空间?
用数学语言说,搜索空间是所有可能候选解的集合 S 。每个解都是一个向量,或者叫一个点。这个向量里的每一个分量,就是你要优化的一个变量。
举个例子,你给一个算法调两个参数:学习率 alpha 和批大小 batch。那你的搜索空间 S 就是:
S = { (alpha, batch) | alpha ∈ [0.0001, 0.1], batch ∈ {16, 32, 64, 128} }
注意看,这里 alpha 是连续的,batch 是离散的。混合类型的空间在实际项目中非常常见。我之前做过一个图像模型调优,参数列表里既有浮点型的衰减系数,又有整数型的网络层数,还有枚举型的激活函数。这种混合空间,定义起来要特别小心。
我习惯把搜索空间拆成四个维度来理解:
| 维度 | 含义 | 实例 |
|---|---|---|
| 定义域 | 每个变量能取哪些值 | alpha ∈ [0.0001, 0.1] |
| 值域映射 | 变量取值后,如何映射到实际配置 | batch=32 → 实际批量大小 32 |
| 有效约束 | 哪些组合在工程上是合法的 | 内存限制下,batch 不能超过 128 |
| 评估指标 | 你怎么判断一个解好不好 | 验证集上的准确率 |
记住:搜索空间的大小 = 每个变量取值数的乘积。如果每个维度多一点,乘积会爆炸式增长。这就是为什么我们常说“维度灾难”。
3.2 定义域:离散、连续与混合
定义域的设定,直接决定了搜索的难度。常见的定义域类型有三种:
- 离散定义域:变量只能取有限个值。比如网络层数只能是 {3, 5, 7},优化器只能是 {Adam, SGD, RMSprop}。离散空间好处理,但容易漏掉中间值。
- 连续定义域:变量在某个区间内可以取任意值。比如学习率 [1e-5, 1e-1]。连续空间理论上无限大,实际搜索时只能采样。
- 混合定义域:既有连续又有离散。这种最复杂,我建议分开处理:先搜离散部分(比如架构),再在选定架构下调连续参数。
来,看一个实际的配置文件示例。这是一个典型的调优任务配置:
# search_space.yaml
search_space:
learning_rate:
type: float
range: [0.0001, 0.1]
scale: log
dropout:
type: float
range: [0.0, 0.5]
scale: linear
num_layers:
type: int
range: [2, 8]
step: 1
optimizer:
type: categorical
values: ['adam', 'sgd', 'rmsprop']
这里,learning_rate 用的是对数尺度。为什么?因为它跨了三个数量级,线性采样会浪费大量计算在 [0.08, 0.1] 这种效果差别不大的区间上。对数尺度就能均匀覆盖小值区域。我个人习惯,只要参数范围超过两个数量级,一律用 log 尺度。
1.3 约束:不是所有点都有效
你在纸上列出的笛卡尔积,并不等于实际可用的搜索空间。工程中有很多隐式约束:
- 资源约束:显存限制、推理时延限制、模型大小限制。
- 逻辑约束:当 A 为真时,B 必须大于某个值;某些参数组合没有意义(比如 drop_out 为0.5 时,layer_norm 可以去掉)。
- 实验约束:某些配置可能直接导致训练崩溃,需要提前过滤。
我曾经接了一个 NLP 模型的调优任务。搜索空间里有一组参数组合:层数=8,隐藏单元=4096,batch=2。看起来都合法对吧?结果一跑,显存直接爆了。从那以后,我每条搜索配置都会先做一个轻量级的可行性检查。
遇到这类约束,我的做法是:先在采样前做一个“合法性过滤器”。
def is_valid_config(config):
# 检查资源约束
if config['batch_size'] * config['seq_len'] * config['hidden_size'] > GPU_MEM_LIMIT:
return False
# 检查逻辑约束
if config['dropout'] == 0.0 and config['layer_norm'] is False:
# 没有 dropout 也没有 layer_norm,训练容易过拟合且不稳定
return False
return True
加了这道过滤,你的搜索过程会少踩很多坑。
1.4 为什么说 Exhaustiveness 是根?
“穷尽性”这词听起来很吓人。但其实逻辑很简单:
- 你的搜索空间必须 包含 真正的最优解。
- 你的搜索方法必须 有机会 访问到那个最优解。
如果空间把最优解排除在外了,那再牛的算法也白搭。比如你的模型最优学习率是 0.003,你的空间定义成 [0.01, 0.1],那再怎么搜也找不到。
那是不是空间越大越好?不是。空间大了,搜索效率直线下降。这里有一个常见的权衡:
“搜索空间的大小与找到最优解的概率成正比,与搜索效率成反比。”
怎么平衡?我一般分两步走:
- 先用粗糙的网格或随机搜索,探一下“好区域”大概在哪。
- 然后缩小空间范围,聚焦到那个区域做精细搜索。
这就好比你在一个陌生城市找一家咖啡馆。你不可能逐条巷子走遍全城。你先走几条主街,确定某个街区咖啡馆最密集,然后再去那几条小巷里细找。
1.5 本章知识体系总览
这一章的内容可以浓缩成下面这张图。我把核心概念和它们之间的关系梳理了一下,方便你对照理解。
嗯,看完这张图你应该能理解了。搜索空间不是单纯的一个集合,它包含了定义域、值域映射、有效约束和评估指标这四个要素。而 Exhaustiveness,就是这四者搭配之后的结果。
1.6 避坑指南:新手最容易犯的三个错
最后,我跟你分享几个我踩过的坑。你刚开始做,可以少走弯路。
- 坑一:空间定义太宽泛。记得上面那个例子吗?连续变量跨几个数量级,但没有用 log 尺度。结果采样点全挤在大值区域,小值区域没怎么探索就浪费了计算资源。
- 坑二:忽略了隐式约束。我调过一个推荐系统,搜索空间里有一组参数组合:embedding_dim=256, hidden_units=[512, 256], batch=4096。理论上是合法的,但实际训练时内存不够,跑了一天,全白费。
- 坑三:没有初始探测。一上来就全量搜索,结果空间太大,搜了一周都没收敛。正确的做法是先做小规模的粗探,缩小范围,再精细化搜索。