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
主要用于优化加密货币交易的强化学习模型。简单来说,它在做以下事情:
- 自动寻找最佳参数:尝试不同的模型参数(如学习率、批量大小等),找出最有效的参数组合
- 评估模型表现:使用 CPCV(组合净化交叉验证)方法评估每组参数的表现
- 保存最佳模型:当找到性能更好的模型时,保存它的参数和权重
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) - 记录最佳结果
具体流程:
-
设置初始参数:
gpu_id = 0 # 使用的 GPU ID name_model = 'ppo' # 使用 PPO 算法 name_test = 'model' # 测试名称
-
启动优化过程:
optimize(name_test, name_model, gpu_id)
-
创建 Optuna study:
study = optuna.create_study( direction='maximize', # 目标是最大化某个指标 sampler=sampler, # 参数采样方法 pruner=optuna.pruners.HyperbandPruner(...) # 提前停止无效参数 )
-
运行优化:
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)定义模型如何看待市场。
参数组合和训练次数
-
参数组合:
learning_rate
有 5 个选项batch_size
有 4 个选项gamma
有 3 个选项net_dimension
有 4 个选项- 其他参数也有各自的选项
-
理论上的总组合:
- 如果完全排列组合,理论上有 5×4×3×4×3×1×3 = 2,160 种可能的参数组合
-
实际训练次数:
- 但在代码中,通过
H_TRIALS
参数限制了实际尝试的次数 - 查看代码中的
optimize
函数:study.optimize( obj_with_argument, n_trials=H_TRIALS, # 试验次数 catch=(ValueError,), callbacks=[save_best_agent] )
H_TRIALS
是在配置文件中设置的,通常远小于全部组合数(可能是几十或几百)
- 但在代码中,通过
Optuna 的智能搜索
Optuna 并不会盲目地尝试所有组合,而是使用智能搜索策略:
-
TPE(Tree-structured Parzen Estimator)采样器:
sampler = optuna.samplers.TPESampler(multivariate=True, seed=SEED_CFG)
- 这是一种贝叶斯优化方法,会根据之前的结果"学习"哪些参数组合可能更好
- 它会优先尝试可能表现更好的参数组合
-
早期剪枝(Pruning):
pruner=optuna.pruners.HyperbandPruner( min_resource=1, max_resource=300, reduction_factor=3 )
- 如果某组参数在训练早期表现很差,Optuna 会提前终止该试验,节省计算资源
举个实际例子
假设 H_TRIALS=100
(最多尝试100次参数组合):
-
第一轮训练:
- Optuna 随机选择一组参数(比如 learning_rate=3e-2, batch_size=512, gamma=0.99…)
- 用这组参数训练模型,得到一个性能分数(例如夏普比率)
-
第二轮训练:
- 基于第一轮的结果,Optuna 选择另一组看起来有希望的参数
- 如果第一轮中 learning_rate=3e-2 表现好,可能会选择相近的值
-
后续训练:
- Optuna 不断调整参数,逐渐找到更好的组合
- 一些表现差的参数组合会被早期剪枝,不会完成完整训练
-
最终结果:
- 100次试验后,Optuna 会返回表现最好的那组参数
- 这组参数很可能比随机搜索或网格搜索找到的更好
参数作用过程
每个参数在训练过程中扮演不同角色:
-
学习率(learning_rate):
- 控制每次更新网络权重的幅度
- 较大值使模型学习更快但可能不稳定
- 较小值学习稳定但速度慢
-
批量大小(batch_size):
- 决定每次更新使用多少样本
- 较大批量训练更稳定但内存消耗大
- 较小批量训练速度快但可能不稳定
-
折扣因子(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. 总结
- Optuna 是自动参数优化工具,帮助寻找最佳模型参数
- CPCV 是一种评估方法,确保金融模型评估的公平性
- 整体流程:尝试不同参数 → 训练模型 → 评估表现 → 找出最佳参数
- 优化目标:默认是使模型的夏普比率超过基准(买入持有策略)尽可能多