FinRL_Crypto源码阅读(1):1_optimize_cpcv.py

目录

optuna中文文档:https://optuna.readthedocs.io/zh-cn/latest/index.html

理解 1_optimize_cpcv.py 中 optuna 相关的操作及整体流程,简单解释这个脚本的工作原理。

1. 整体功能介绍

1_optimize_cpcv.py 主要用于优化加密货币交易的强化学习模型。简单来说,它在做以下事情:

  1. 自动寻找最佳参数:尝试不同的模型参数(如学习率、批量大小等),找出最有效的参数组合
  2. 评估模型表现:使用 CPCV(组合净化交叉验证)方法评估每组参数的表现
  3. 保存最佳模型:当找到性能更好的模型时,保存它的参数和权重

2. 依赖环境安装

大家都应该使用的python虚拟环境(anaconda,miniconda等)吧。例如:

conda create -n finrl-crypto python=3.9

安装pytorch(优先)

优先安装pytorch,因为如果使用项目中版本的pytorch,可能不能和自己cuda匹配,导致无法使用GPU算力。

  • 查看本机CUDA信息(Windows)
nvcc -V

  • 根据机器和CUDA信息下载对应版本的pytorch

官网:https://pytorch.org/

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128 --default-timeout=1000

# 验证
python 

import torch

torch.cuda.is_available()

修改项目依赖(requirements.txt)

为什么要修改,因为我的环境存在依赖冲突,要解决很麻烦。如果没有冲突,不用修改。

gym==0.25.0
joblib==1.1.1
TA_Lib==0.5.1
# torch==1.9.1

执行命令行,拉取依赖:

# ta-lib直接使用pip安装容易报错
# 可能需要科学 上网
conda install -c conda-forge ta-lib=0.5.1 pybullet box2d-py

pip install -r requirements.txt

其他问题

Numpy可能需要降级

pip uninstall numpy
pip install numpy==1.21.0

stockstats包import可能报错 get_date_from_diff()不存在,改成from_diff()

3. Optuna 是什么?

Optuna 是一个自动参数优化框架,可以简单理解为:

  • 它会自动尝试不同的参数组合
  • 评估每组参数的效果
  • 逐渐找到最佳参数组合

就像你在手动调整电视频道找最清晰信号,但 Optuna 能自动且更有效地做这件事。

4. 代码结构和流程

主程序入口
  ↓
优化函数 (optimize)
  ↓
目标函数 (objective) - 每组参数的评估过程
  ↓
训练测试函数 (train_and_test) - 训练和测试当前参数
  ↓
保存最佳模型 (save_best_agent) - 记录最佳结果

具体流程:

  1. 设置初始参数

    gpu_id = 0  # 使用的 GPU ID
    name_model = 'ppo'  # 使用 PPO 算法
    name_test = 'model'  # 测试名称
    
  2. 启动优化过程

    optimize(name_test, name_model, gpu_id)
    
  3. 创建 Optuna study

    study = optuna.create_study(
        direction='maximize',  # 目标是最大化某个指标
        sampler=sampler,  # 参数采样方法
        pruner=optuna.pruners.HyperbandPruner(...)  # 提前停止无效参数
    )
    
  4. 运行优化

    study.optimize(
        obj_with_argument,  # 评估函数
        n_trials=H_TRIALS,  # 尝试次数
        catch=(ValueError,),  # 捕获异常
        callbacks=[save_best_agent]  # 保存最佳模型的回调函数
    )
    

5. 关键模块详解

5.1. 参数采样 (sample_hyperparams)

这个函数决定要尝试哪些参数组合:

def sample_hyperparams(trial):
    """
    为当前试验采样超参数
    
    参数:
        trial: Optuna trial 对象
        
    返回:
        sampled_erl_params: 强化学习参数字典
        sampled_env_params: 环境参数字典
    """
    # 计算最小 episode 步数
    average_episode_step_min = no_candles_for_train + 0.25 * no_candles_for_train
    
    # 采样强化学习参数
    sampled_erl_params = {
        "learning_rate": trial.suggest_categorical("learning_rate", [3e-2, 2.3e-2, 1.5e-2, 7.5e-3, 5e-6]),  # 学习率
        "batch_size": trial.suggest_categorical("batch_size", [512, 1280, 2048, 3080]),  # 批量大小
        "gamma": trial.suggest_categorical("gamma", [0.85, 0.99, 0.999]),  # 折扣因子
        "net_dimension": trial.suggest_categorical("net_dimension", [2 ** 9, 2 ** 10, 2 ** 11, 2 ** 12]),  # 网络维度
        "target_step": trial.suggest_categorical("target_step",  # 目标步数
                                                 [average_episode_step_min, round(1.5 * average_episode_step_min),
                                                  2 * average_episode_step_min]),
        "eval_time_gap": trial.suggest_categorical("eval_time_gap", [60]),  # 评估时间间隔
        "break_step": trial.suggest_categorical("break_step", [3e4, 4.5e4, 6e4])  # 训练提前终止步数
    }

    # 采样环境标准化和历史长度参数
    sampled_env_params = {
        "lookback": trial.suggest_categorical("lookback", [1]),  # 观察窗口长度
        "norm_cash": trial.suggest_categorical("norm_cash", [2 ** -12]),  # 现金归一化系数
        "norm_stocks": trial.suggest_categorical("norm_stocks", [2 ** -8]),  # 股票归一化系数
        "norm_tech": trial.suggest_categorical("norm_tech", [2 ** -15]),  # 技术指标归一化系数
        "norm_reward": trial.suggest_categorical("norm_reward", [2 ** -10]),  # 奖励归一化系数
        "norm_action": trial.suggest_categorical("norm_action", [10000])  # 动作归一化系数
    }
    return sampled_erl_params, sampled_env_params

这里定义了程序会尝试的各种参数值。类似于你告诉厨师:“试试这些不同的调料比例,看哪个最好吃”。

参数详解

参数分为两个部分,强化学习参数和环境参数。 强化学习参数

  • learning_rate (学习率):决定模型每次更新时参数调整的幅度,值域范围:从 0.00005 (5e-6) 到 0.03 (3e-2)。较大的学习率:模型学习速度快,但可能导致不稳定或无法收敛;较小的学习率:学习稳定,但是速度慢,可能会陷入局部最优。影响模型对市场变化的适应速度,较小值适合稳定市场,较大值适合快速变化市场。
  • batch_size(批量大小):每次模型更新时使用的样本数量,值域范围:512 到 3080 个样本。较大的批量:训练更稳定,梯度估计更准确,但内存消耗更大;较小的批量:训练速度快,适应性好,但梯度估计噪声大。影响模型对不同市场样本的概括能力,较大批量有助于学习整体市场趋势。
  • gamma (折扣因子):决定未来奖励的重要性,值越高表示更关注长期收益,值域范围:0.85 到 0.999。接近1的值:模型更注重长期回报,决策更有远见;较小的值:模型更关注短期回报,决策更即时。影响交易策略的时间跨度,高值适合长期持有策略,低值适合短期交易策略。
  • net_dimension (网络维度):神经网络隐藏层的节点数量,值域范围:512 (2^9) 到 4096 (2^12)。较大的网络:表示能力强,可以学习复杂模式,但容易过拟合;泛化能力好,训练速度快,但可能无法捕捉复杂模式。影响模型识别市场模式的能力,更大的网络可能更好地分析复杂市场关系。
  • target_step (目标步数):在更新模型前收集的交互步数,值域范围:基于训练样本数量动态计算,约为训练样本的1.25倍到2倍。较大值:更多样本,更稳定的学习,但更新频率低;较小值:更新频繁,但样本多样性可能不足。影响模型学习交易决策的样本量,较大值适合学习长期市场行为。
  • eval_time_gap (评估时间间隔):评估模型性能的频率(每60步评估一次),值域范围:固定为60。决定多久检查一次模型的表现,确定了回测频率,影响模型调整和评估的节奏。
  • break_step (训练提前终止步数):训练的最大步数,达到此步数后停止训练,值域范围:30,000 到 60,000 步。训练时间更长,可能学习更好的策略,训练速度快,但可能学习不充分,影响模型的训练深度,较大值给予模型更多机会学习市场模式。

环境参数

  • lookback (观察窗口长度):模型做决策时考虑的历史状态数量,值域范围:固定为1(只看当前状态)。决定模型是否考虑历史信息,影响模型对历史价格走势的记忆能力,值为1表示只考虑当前市场状态。
  • norm_cash (现金归一化系数):将现金金额缩放到适合神经网络处理的范围,值域范围:固定为2^-12 (约0.00024)。将大金额值映射到较小范围,以便神经网络处理,帮助模型处理不同规模的现金金额,保持数值稳定性。
  • norm_stocks (股票归一化系数):将持仓量缩放到适合神经网络处理的范围,值域范围:固定为2^-8 (约0.0039)。将不同规模的持仓量映射到统一范围,使模型能处理不同规模的加密货币持仓量。
  • norm_tech (技术指标归一化系数):将技术指标值缩放到适合神经网络处理的范围,值域范围:固定为2^-15 (约0.00003)。使技术指标值(如RSI、MACD等)保持在合适范围,使不同规模和单位的技术指标能被模型一致处理。
  • norm_reward (奖励归一化系数):将强化学习奖励信号缩放到合适范围,值域范围:固定为2^-10 (约0.00098)。使奖励信号(通常基于利润/亏损)数值稳定,影响模型如何看待交易收益,避免极端收益导致的训练不稳定。
  • norm_action (动作归一化系数):将模型输出的动作(如买卖量)缩放到实际交易量,值域范围:固定为10000。将神经网络的小数输出转换为有意义的交易动作大小,决定模型产生的交易量,较大值允许更大的仓位调整。

这些参数共同定义了强化学习模型的学习过程和交易环境的特性,通过优化这些参数,可以找到最适合特定市场的交易策略。学习参数(学习率、批量大小等)影响模型学习能力,归一化参数(norm_)确保数据适合神经网络处理,环境参数(lookback)定义模型如何看待市场。

参数组合和训练次数

  1. 参数组合

    • learning_rate 有 5 个选项
    • batch_size 有 4 个选项
    • gamma 有 3 个选项
    • net_dimension 有 4 个选项
    • 其他参数也有各自的选项
  2. 理论上的总组合

    • 如果完全排列组合,理论上有 5×4×3×4×3×1×3 = 2,160 种可能的参数组合
  3. 实际训练次数

    • 但在代码中,通过 H_TRIALS 参数限制了实际尝试的次数
    • 查看代码中的 optimize 函数:
      study.optimize(
          obj_with_argument,
          n_trials=H_TRIALS,  # 试验次数
          catch=(ValueError,),
          callbacks=[save_best_agent]
      )
      
    • H_TRIALS 是在配置文件中设置的,通常远小于全部组合数(可能是几十或几百)

Optuna 的智能搜索

Optuna 并不会盲目地尝试所有组合,而是使用智能搜索策略

  1. TPE(Tree-structured Parzen Estimator)采样器

    sampler = optuna.samplers.TPESampler(multivariate=True, seed=SEED_CFG)
    
    • 这是一种贝叶斯优化方法,会根据之前的结果"学习"哪些参数组合可能更好
    • 它会优先尝试可能表现更好的参数组合
  2. 早期剪枝(Pruning)

    pruner=optuna.pruners.HyperbandPruner(
        min_resource=1,
        max_resource=300,
        reduction_factor=3
    )
    
    • 如果某组参数在训练早期表现很差,Optuna 会提前终止该试验,节省计算资源

举个实际例子

假设 H_TRIALS=100(最多尝试100次参数组合):

  1. 第一轮训练

    • Optuna 随机选择一组参数(比如 learning_rate=3e-2, batch_size=512, gamma=0.99…)
    • 用这组参数训练模型,得到一个性能分数(例如夏普比率)
  2. 第二轮训练

    • 基于第一轮的结果,Optuna 选择另一组看起来有希望的参数
    • 如果第一轮中 learning_rate=3e-2 表现好,可能会选择相近的值
  3. 后续训练

    • Optuna 不断调整参数,逐渐找到更好的组合
    • 一些表现差的参数组合会被早期剪枝,不会完成完整训练
  4. 最终结果

    • 100次试验后,Optuna 会返回表现最好的那组参数
    • 这组参数很可能比随机搜索或网格搜索找到的更好

参数作用过程

每个参数在训练过程中扮演不同角色:

  1. 学习率(learning_rate):

    • 控制每次更新网络权重的幅度
    • 较大值使模型学习更快但可能不稳定
    • 较小值学习稳定但速度慢
  2. 批量大小(batch_size):

    • 决定每次更新使用多少样本
    • 较大批量训练更稳定但内存消耗大
    • 较小批量训练速度快但可能不稳定
  3. 折扣因子(gamma):

    • 控制模型对长期和短期回报的偏好
    • 接近1的值更看重长期回报
    • 较小值更看重短期回报

这些参数相互影响,一般没有单一"最佳"值,而是需要找到一组共同作用良好的参数组合。

总结

  • 不需要手动尝试所有2,160种组合,Optuna会智能地搜索参数空间
  • 实际训练次数由 H_TRIALS 决定,通常远小于全部可能组合
  • 通过TPE采样器和早期剪枝,Optuna能高效找到好的参数组合
  • 每次训练中,参数共同作用形成模型的学习行为和决策风格

5.2. 目标函数 (objective)

评估每组参数的表现:

def objective(trial, name_test, model_name, cwd, res_timestamp, gpu_id):
    # 采样超参数集
    erl_params, env_params = sample_hyperparams(trial)
    
    # 加载数据
    data_from_processor, price_array, tech_array, time_array = load_saved_data(...)
    
    # 设置交叉验证
    cpcv, env, data, ... = setup_CPCV(...)
    
    # CV 循环
    sharpe_list_bot = []  # 模型夏普比率
    sharpe_list_ewq = []  # 基准夏普比率
    
    for split, (train_indices, test_indices) in enumerate(cpcv.split(...)):
        # 训练和测试模型
        sharpe_bot, sharpe_eqw, drl_rets_tmp = train_and_test(...)
        
        # 记录结果
        sharpe_list_ewq.append(sharpe_eqw)
        sharpe_list_bot.append(sharpe_bot)
    
    # 返回优化目标:模型平均夏普比率 - 基准平均夏普比率
    return np.mean(sharpe_list_bot) - np.mean(sharpe_list_ewq)

这个函数相当于:厨师根据给定配方做菜,然后品尝评分,返回一个分数告诉 Optuna 这个配方有多好。

5.3. CPCV 交叉验证

CPCV(组合净化交叉验证)是一种特殊的评估方法,专为金融时间序列设计:

def setup_CPCV(...):
    # 设置组合净化交叉验证
    num_paths = NUM_PATHS
    k_test_groups = K_TEST_GROUPS
    n_total_groups = num_paths + 1
    
    # 计算所有可能的组合
    n_splits = np.array(list(itt.combinations(np.arange(n_total_groups), k_test_groups)))
    
    # 创建交叉验证对象
    cv = CombPurgedKFoldCV(...)
    
    # 计算回测路径
    is_test, paths, _ = back_test_paths_generator(...)
    
    return cv, env, data, ...

这部分确保模型评估是公平的,防止"偷看未来"的问题。类似于考试:先在训练集学习,再在测试集检验,但采用特殊方法避免金融数据的泄露问题。

6. 如何修改程序

基于你的理解,这里有几种可能的修改方案:

6.1. 修改参数搜索范围

如果你想尝试不同的参数值:

def sample_hyperparams(trial):
    sampled_erl_params = {
        # 修改学习率范围
        "learning_rate": trial.suggest_categorical("learning_rate", [1e-2, 5e-3, 1e-3]),
        # 修改批量大小
        "batch_size": trial.suggest_categorical("batch_size", [256, 512, 1024]),
        # 其他参数...
    }
    # ...

6.2. 增加并行度提高速度

study.optimize() 中添加 n_jobs 参数:

study.optimize(
    obj_with_argument,
    n_trials=H_TRIALS,
    catch=(ValueError,),
    callbacks=[save_best_agent],
    n_jobs=4  # 使用4个进程并行优化
)

6.3. 修改评估指标

如果你想使用其他指标(而不是夏普比率差值):

# 在 objective 函数最后修改返回值
return np.mean(sharpe_list_bot)  # 只关注模型表现,不与基准比较

或者:

# 返回最大回撤或其他指标
return -np.mean(max_drawdown_list)  # 负号是因为最小化最大回撤

7. 总结

  1. Optuna 是自动参数优化工具,帮助寻找最佳模型参数
  2. CPCV 是一种评估方法,确保金融模型评估的公平性
  3. 整体流程:尝试不同参数 → 训练模型 → 评估表现 → 找出最佳参数
  4. 优化目标:默认是使模型的夏普比率超过基准(买入持有策略)尽可能多