4、随机搜索法:随机采样原理、与网格搜索对比、收敛速度分析、Python代码实现
聊完了网格搜索,咱们来聊聊随机搜索。
说实话,我第一次接触随机搜索时,心里是有点抵触的。你想啊,网格搜索好歹是"地毯式轰炸",每个格子都检查一遍。随机搜索呢?随机扔几个点,就敢说找到最优参数了?这不是碰运气吗?
后来我在一个CTA策略项目里被网格搜索坑惨了——参数空间是5维的,网格点数量直接爆炸,跑了整整两天两夜还没出结果。无奈之下试了随机搜索,结果你猜怎么着?3个小时就找到了比网格搜索更好的参数组合。
嗯,从那以后,我再也不敢小看随机搜索了。
4.1 随机采样原理
随机搜索的核心思想其实很简单:在参数空间里随机撒点,然后评估每个点的表现。
但这里有个关键点——随机采样不是"瞎蒙"。它背后有严格的概率论支撑。
具体来说,随机搜索的采样过程是这样的:
- 假设我们有d个参数需要优化
- 每个参数都有各自的取值范围(比如学习率0.001~0.1,树深度3~20)
- 我们从每个参数的取值范围内,按照均匀分布随机抽取一个值
- 把这些值组合起来,就形成了一个采样点
- 重复N次,得到N个采样点
我习惯用numpy.random.uniform或者scipy.stats.uniform来做这件事。如果你对某个参数有先验知识,也可以用其他分布,比如对数均匀分布——这在学习率优化中特别常见。
4.2 与网格搜索对比
咱们来做个直观对比。我直接上表格,这样更清楚:
| 对比维度 | 网格搜索 | 随机搜索 |
|---|---|---|
| 采样方式 | 均匀网格,系统化 | 随机采样,概率化 |
| 参数维度 | 维度高时点数爆炸 | 维度高时依然可控 |
| 计算成本 | 随维度指数增长 | 与采样次数线性相关 |
| 最优解发现 | 依赖网格密度 | 概率上保证收敛 |
| 并行化 | 容易,但点数固定 | 容易,可随时增加采样 |
| 适用场景 | 低维度(≤3),小范围 | 高维度(≥4),大范围 |
为什么会这样?说白了,网格搜索在低维度时表现不错,但一旦参数维度超过3,网格点数就会爆炸式增长。比如每个维度取10个值,3维就是1000个点,5维就是10万个点——你算算得跑多久?
而随机搜索呢?不管参数有多少维,你只需要决定采样多少个点。100个点就是100次评估,1000个点就是1000次评估。计算成本完全可控。
我记得有一次做高频策略的参数优化,参数维度是7维。网格搜索需要10^7 = 1000万个点,根本不可能。随机搜索只用了5000个点,就找到了一个不错的局部最优解。虽然不敢说全局最优,但实盘表现已经比之前的策略好很多了。
4.3 收敛速度分析
收敛速度这个问题,咱们得从两个角度看:
第一,理论上的收敛速度。
随机搜索的收敛速度是O(N^{-1/d}),其中N是采样次数,d是参数维度。你看,维度d在分母上,维度越高,收敛越慢。但关键是——它至少能收敛!
网格搜索呢?它的收敛速度是O(N^{-2/d})。看起来比随机搜索快,但前提是你要知道最优解在哪个网格里。如果最优解恰好落在网格之间,那网格搜索就永远找不到它。
第二,实际应用中的收敛表现。
我在实战中发现一个规律:当参数维度≤3时,网格搜索通常比随机搜索快;当维度≥4时,随机搜索反而更快找到好解。
为什么会这样?因为高维度下,网格搜索的大部分计算都浪费在了"不重要的区域"。而随机搜索虽然看起来"乱来",但它能更快地覆盖整个参数空间。
我曾经做过一个对比实验:在5维参数空间里,网格搜索用了10000个点,随机搜索用了2000个点。结果随机搜索找到的最优解,比网格搜索的好12%。你说气不气人?
4.4 Python代码实现
好了,理论说完了,咱们上代码。我直接给一个完整的随机搜索实现,你可以直接拿去用。
import numpy as np
import pandas as pd
from itertools import product
from typing import Dict, List, Callable, Tuple
import time
class RandomSearchOptimizer:
"""
随机搜索优化器
用于数字货币量化策略的参数优化
"""
def __init__(self, param_space: Dict,
objective_func: Callable,
n_iterations: int = 100,
random_seed: int = 42):
"""
参数:
- param_space: 参数字典,格式为 {'param_name': (min, max)}
- objective_func: 目标函数,输入参数字典,输出评估指标
- n_iterations: 随机采样次数
- random_seed: 随机种子,保证结果可复现
"""
self.param_space = param_space
self.objective_func = objective_func
self.n_iterations = n_iterations
self.random_seed = random_seed
# 设置随机种子
np.random.seed(self.random_seed)
# 存储结果
self.results = []
self.best_params = None
self.best_score = -np.inf
def _sample_random_params(self) -> Dict:
"""从参数空间中随机采样一组参数"""
sampled_params = {}
for param_name, (min_val, max_val) in self.param_space.items():
# 使用均匀分布采样
sampled_params[param_name] = np.random.uniform(min_val, max_val)
return sampled_params
def optimize(self, verbose: bool = True) -> Tuple[Dict, float]:
"""
执行随机搜索优化
返回:
- best_params: 最优参数组合
- best_score: 最优得分
"""
start_time = time.time()
for i in range(self.n_iterations):
# 随机采样一组参数
params = self._sample_random_params()
# 评估目标函数
try:
score = self.objective_func(params)
except Exception as e:
if verbose:
print(f" 第{i+1}次采样评估失败: {e}")
continue
# 记录结果
self.results.append({
'iteration': i + 1,
'params': params,
'score': score
})
# 更新最优解
if score > self.best_score:
self.best_score = score
self.best_params = params.copy()
if verbose:
print(f" 第{i+1}次采样: 发现新最优! 得分={score:.4f}")
elapsed_time = time.time() - start_time
if verbose:
print(f"\n随机搜索完成!")
print(f"总采样次数: {self.n_iterations}")
print(f"有效评估次数: {len(self.results)}")
print(f"耗时: {elapsed_time:.2f}秒")
print(f"最优得分: {self.best_score:.4f}")
print(f"最优参数: {self.best_params}")
return self.best_params, self.best_score
def get_results_dataframe(self) -> pd.DataFrame:
"""将结果转换为DataFrame,方便分析"""
records = []
for r in self.results:
record = {'iteration': r['iteration'], 'score': r['score']}
record.update(r['params'])
records.append(record)
return pd.DataFrame(records)
# ============ 使用示例 ============
# 假设我们要优化一个简单的移动平均线策略
# 参数: 短周期(5-50), 长周期(20-200), 止损比例(0.01-0.05)
def dummy_strategy(params):
"""
模拟策略评估函数
实际使用时替换为你的回测函数
"""
short_window = int(params['short_window'])
long_window = int(params['long_window'])
stop_loss = params['stop_loss']
# 模拟一个简单的评估逻辑
# 实际项目中这里应该是完整的回测代码
if short_window >= long_window:
return -100 # 无效参数,给一个很低的分数
# 模拟夏普比率
sharpe = 1.5 + 0.01 * (50 - short_window) + 0.005 * (long_window - 100)
sharpe -= 10 * abs(stop_loss - 0.03) # 止损偏离0.03会扣分
# 加入随机噪声,模拟真实回测的波动
noise = np.random.normal(0, 0.1)
return sharpe + noise
# 定义参数空间
param_space = {
'short_window': (5, 50),
'long_window': (20, 200),
'stop_loss': (0.01, 0.05)
}
# 创建优化器
optimizer = RandomSearchOptimizer(
param_space=param_space,
objective_func=dummy_strategy,
n_iterations=200,
random_seed=42
)
# 执行优化
best_params, best_score = optimizer.optimize(verbose=True)
# 查看结果
results_df = optimizer.get_results_dataframe()
print("\n前5次采样结果:")
print(results_df.head())
# 查看最优参数
print(f"\n最优参数组合:")
for param, value in best_params.items():
if param in ['short_window', 'long_window']:
print(f" {param}: {int(value)}")
else:
print(f" {param}: {value:.4f}")
print(f"最优得分: {best_score:.4f}")
dummy_strategy只是一个模拟函数。实际使用时,你需要替换成你自己的回测函数。我曾经犯过一个错误——在随机搜索里用了带随机种子的回测函数,结果每次评估结果都不一样,导致优化结果完全不可靠。记得在回测函数里固定随机种子!
代码跑完之后,你会得到一个DataFrame,里面记录了每次采样的参数和得分。我习惯用这个DataFrame做进一步分析,比如:
- 画个散点图,看看参数和得分之间的关系
- 找找得分最高的前10个参数组合,看看有没有规律
- 分析一下哪些参数对结果影响最大
这些分析能帮你更好地理解策略的参数空间,为后续的精细优化打基础。
嗯,随机搜索就聊到这儿。说白了,它不是什么高大上的算法,但胜在简单、实用、可扩展。在参数维度高、计算资源有限的情况下,随机搜索往往是最务实的选择。
公众号:蓝海资料掘金营,微信deep3321