4、随机搜索法:随机采样原理、与网格搜索对比、收敛速度分析、Python代码实现

聊完了网格搜索,咱们来聊聊随机搜索。

说实话,我第一次接触随机搜索时,心里是有点抵触的。你想啊,网格搜索好歹是"地毯式轰炸",每个格子都检查一遍。随机搜索呢?随机扔几个点,就敢说找到最优参数了?这不是碰运气吗?

后来我在一个CTA策略项目里被网格搜索坑惨了——参数空间是5维的,网格点数量直接爆炸,跑了整整两天两夜还没出结果。无奈之下试了随机搜索,结果你猜怎么着?3个小时就找到了比网格搜索更好的参数组合。

嗯,从那以后,我再也不敢小看随机搜索了。

4.1 随机采样原理

随机搜索的核心思想其实很简单:在参数空间里随机撒点,然后评估每个点的表现。

但这里有个关键点——随机采样不是"瞎蒙"。它背后有严格的概率论支撑。

具体来说,随机搜索的采样过程是这样的:

  • 假设我们有d个参数需要优化
  • 每个参数都有各自的取值范围(比如学习率0.001~0.1,树深度3~20)
  • 我们从每个参数的取值范围内,按照均匀分布随机抽取一个值
  • 把这些值组合起来,就形成了一个采样点
  • 重复N次,得到N个采样点

我习惯用numpy.random.uniform或者scipy.stats.uniform来做这件事。如果你对某个参数有先验知识,也可以用其他分布,比如对数均匀分布——这在学习率优化中特别常见。

小技巧: 我个人习惯在随机搜索前先做一次"小规模预跑"。比如先随机采20个点,看看参数空间的"地形"大概什么样。如果发现某些区域表现明显更好,我会调整采样分布,把更多采样点集中到这些区域。

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时,随机搜索反而更快找到好解。

为什么会这样?因为高维度下,网格搜索的大部分计算都浪费在了"不重要的区域"。而随机搜索虽然看起来"乱来",但它能更快地覆盖整个参数空间。

核心结论: 随机搜索在参数维度较高时(≥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