news 2026/8/6 1:28:23

使用 DVC 的实验跟踪跟踪您的回测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 DVC 的实验跟踪跟踪您的回测

原文:towardsdatascience.com/keep-track-of-your-backtests-with-dvcs-experiment-tracking-38977cbba4a9

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/ed1c7931f71cf9a725f3e152ad579a20.png

使用 Midjourney 生成的图像

在本系列的先前部分中,我展示了您如何利用 DVC 进行实验跟踪。只需一点额外的努力,您就可以停止在纸上或电子表格中记录实验结果。

您可以在以下位置找到本系列的先前部分:

  • 第一部分:将 VS Code 变成 ML 实验的一站式商店

  • 第二部分:使用实时图表增强您的 ML 实验工作流程

  • 第三部分:使用 DVC 进行实验跟踪的最简指南

在前面的部分中,我们看到了几个用于分类问题的实验跟踪示例。在时间序列预测中,我们使用不同的方法来验证我们的模型。因此,我们也可能想要跟踪更多关于我们回测的元数据,并在多个聚合组中保存分数。

在我关于回测的上一篇文章中,我创建了一个自定义类,它允许我轻松评估我的预测模型。然后,我在 Jupyter Notebook 中运行了多次回测,并将结果存储在一个大字典中。虽然这种方法可以工作,但它肯定不会随着实验数量的增加而很好地扩展。

正因如此,在这篇文章中,我将向您展示如何通过使用 DVC 跟踪回测来改进这种方法。

对回测的快速回顾

回测(也称为历史回溯时间序列交叉验证)是一组旨在满足时间序列特定要求的验证方法。与交叉验证类似,回测的目标是在模型部署后获得一个可靠的性能估计。我们还可以使用这些方法进行超参数调整和特征选择。

回测的想法是复制一个现实场景。训练数据应该对应于在做出预测时可用于训练模型的数据。验证集应该反映在部署该模型后我们会遇到的数据。

下面我们展示了一个称为向前推进验证(或扩展窗口验证)的方法图,它遵循我们刚刚描述的特征。在随后的每个时间点,我们都有更多数据来训练我们的模型,相应地,我们的测试集也以相同的时间间隔前进。这种验证保留了时间序列的时间顺序。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/c6083c25db79a042c4a853533ad1e902.png

向前推进(扩展窗口)验证

在实践中跟踪我们的回测

为了保持简单,我们将遵循我们在最小化实验跟踪方法中采取的步骤。因此,我们将只关注跟踪我们的实验和设置它们。然而,我强烈建议你复制系列前几部分中我们采取的所有步骤,包括设置数据版本化和参数/度量值的适当存储。

下面,你可以看到我们项目的结构,它基本上由 5 个 Python 脚本组成。

📦 backtesting ┣ 📂.dvc ┣ 📂 data ┃ ┗ 📜 time_series.csv ┣ 📂 src ┃ ┣ 📜 backtester.py ┃ ┣ 📜 config.py ┃ ┣ 📜 generate_data.py ┃ ┣ 📜 model_definitions.py ┃ ┗ 📜 run_backtest.py ┣ 📜.dvcignore ┣ 📜.gitignore ┣ 📜 README.md ┣ 📜 dvc.yaml ┗ 📜 requirements.txt

我们将按项目需要的顺序通过所有 5 个脚本。

1. 生成数据

generate_data.py脚本创建一个人工的时间序列,然后将其存储在data目录中。我们将使用具有明显季节成分的 4 年每日数据。

为了简洁,我将不会展示我是如何生成数据的。你可以查看确切的代码这里。下面你可以看到我们生成的时序看起来像什么。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/b84204f960663cb24235ae41c8030fc8.png

为此项目生成的时间序列

2. 定义回测类

下一个脚本是backtester.py,其中包含TSBacktester类。在这个类中,我们封装了处理我们模型验证和保存其结果所需的所有逻辑。

由于这是一个相当大的文件,我将一次分解一个方法。

importpandasaspdclassTSBacktester:""" A custom time series backtester class for evaluating forecasting models. """def__init__(self,pred_func,start_date,end_date,backtest_freq,data_freq,forecast_horizon,rolling_window_size=None,):self.pred_func=pred_func self.start_date=start_date self.end_date=end_date self.backtest_freq=backtest_freq self.data_freq=data_freq self.forecast_horizon=forecast_horizon self.rolling_window_size=rolling_window_size self.backtest_df=Nonedefrun_backtest(self,df,target_col,features=None):""" Run the time series backtest using the specified parameters. """# details will follow belowdefevaluate_backtest(self,metrics,model_name,agg_col="horizon"):""" Evaluates the backtest using specified performance metrics. """# details will follow below

我们从run_backtest方法开始。想法是遍历回测中的每个预测日期。对于每个日期,我们将训练集(包含在做出预测时所有可用的信息)与验证集(由预测范围确定)分开。随后,我们生成预测并存储预测值和实际值。在最后一步,我们将所有单独的 DataFrame 合并成一个包含整个回测中所有预测的 DataFrame。

defrun_backtest(self,df,target_col,features=None):""" Run the time series backtest using the specified parameters. """ts_df=df.copy()fcst_dates=pd.date_range(self.start_date,self.end_date,freq=self.backtest_freq)backtest_list=[]forforecast_dateinfcst_dates:test_ind=pd.date_range(forecast_date,periods=self.forecast_horizon,freq=self.data_freq)X_train=ts_df.loc[ts_df.index<forecast_date].copy()ifself.rolling_window_sizeisnotNone:X_train=X_train.iloc[-self.rolling_window_size:]y_train=X_train.pop(target_col)X_test=ts_df.loc[ts_df.index.isin(test_ind)].copy()y_test=X_test.pop(target_col)# get predictionsy_pred=self.pred_func(X_train,y_train,X_test,self.forecast_horizon,features)pred_df=pd.DataFrame({"forecast_date":forecast_date,"report_date":y_test.index,"forecast":y_pred,"actual":y_test,})backtest_list.append(pred_df)backtest_df=pd.concat(backtest_list,ignore_index=True)# add some columns for potential score calculationbacktest_df["horizon"]=(backtest_df["report_date"]-backtest_df["forecast_date"]).dt.days self.backtest_df=backtest_df

evaluate_backtest方法中,我们使用先前生成的 DataFrame 来计算各种评估指标。可以通过提供一个包含指标名称和用于其计算的相应函数的字典来指定这些指标。然后,我们分别对聚合列(例如,预测范围)中的每个值以及整体计算每个请求的指标。

defevaluate_backtest(self,metrics,model_name,agg_col="horizon"):""" Evaluates the backtest using specified performance metrics. """ifself.backtest_dfisNone:raiseValueError("Backtest was not yet executed! Please run it before evaluating")metadata_dict={}score_dict={}# adding some details about the backtestmetadata_dict["model_name"]=model_name metadata_dict["start_date"]=self.start_date metadata_dict["end_date"]=self.end_date metadata_dict["backtest_freq"]=self.backtest_freq metadata_dict["forecast_horizon"]=self.forecast_horizon metadata_dict["validation_type"]=("rolling"ifself.rolling_window_sizeisnotNoneelse"expanding")metadata_dict["rolling_window_size"]=self.rolling_window_size# preparing for scoringbacktest_df=self.backtest_df.copy()grouped=backtest_df.groupby(agg_col)# calculate scores and store them in a dictformetric,metric_funcinmetrics.items():score_dict[f"{metric}_total"]=round(metric_func(backtest_df["actual"],backtest_df["forecast"]),4)forgroup,group_dfingrouped:score_dict[f"{metric}_{agg_col}_{group}"]=round(metric_func(group_df["actual"],group_df["forecast"]),4)returnmetadata_dict,score_dict

我们决定分别跟踪总分和分组得分,因为我们可能对更细粒度的细节感兴趣。例如,我们关心模型在整个预测范围内的整体准确性。然而,我们也可能跟踪每个预测范围的得分,因为对于我们的业务案例,某些预测范围可能比其他预测范围更重要。同样,我们可以跟踪每个工作日的得分,以研究我们的模型是否在预测特定工作日时遇到困难。

此外,我们还存储了回测的元数据,包括开始和结束日期、是否使用滚动或扩展窗口验证的信息、滚动窗口的潜在大小、模型的名称等。保存元数据的目标是帮助我们轻松比较实验,一旦我们开始运行数十个实验,有时使用不同的日期范围或验证方法。

3. 定义模型

我们将比较四个“预测模型”的性能:

  • 天真的预测:在这种方法中,预测值等于预测时刻所知的最后一个已知值。

  • 平均预测:这种预测等于训练集的平均值。

  • 以月份虚拟变量作为特征的线性回归模型。

  • 以月份虚拟变量作为特征的随机森林模型。

前两个模型将作为简单的基准,而后两个模型旨在真正学习一些东西。然而,请记住,这些模型绝对不是好的模型。我们只使用它们来展示我们如何跟踪回测的结果。

model_definitions.py脚本中,我们定义了用于获取预测的函数。我们选择这种方法以保持我们回测类的灵活性。这样,我们可以将任何类型的机器学习模型包装成一个返回预期预测范围预测的函数。

importpandasaspdfromsklearn.linear_modelimportLinearRegressionfromsklearn.ensembleimportRandomForestRegressordefnaive_forecast(X_train,y_train,X_test,horizon,features):y_pred=[y_train.iloc[-1]]*horizonreturny_preddefmean_forecast(X_train,y_train,X_test,horizon,features):y_pred=[y_train.mean()]*horizonreturny_preddeflinear_model(X_train,y_train,X_test,horizon,features):iffeaturesisnotNone:X_train=X_train[features]X_test=X_test[features]model=LinearRegression()model.fit(X_train,y_train)y_pred=model.predict(X_test)returny_preddefrf_model(X_train,y_train,X_test,horizon,features):iffeaturesisnotNone:X_train=X_train[features]X_test=X_test[features]model=RandomForestRegressor()model.fit(X_train,y_train)y_pred=model.predict(X_test)returny_pred

4. 设置配置

config.py文件中,我们存储了我们的回测配置。我们在此指定如下设置:

  • 回测的开始和结束日期,

  • 我们验证框架中的步长,

  • 数据的频率,

  • 预测范围,

  • 我们是使用扩展窗口还是滚动窗口,

  • 评估指标。

这也是我们选择要评估哪个模型(SELECTED_MODEL变量)以及我们想要绘制的哪些指标(METRICS_TO_PLOT)的地方。

fromsklearn.metricsimportmean_absolute_error,mean_squared_errorfromsrc.model_definitionsimport*# backtesting setupBT_START_DATE="2023-01-01"BT_END_DATE="2023-12-24"BACKTEST_FREQ="7D"DATA_FREQ="1D"FCST_HORIZON=3ROLLING_WINDOW_SIZE=NoneBT_METRICS={"mse":mean_squared_error,"mae":mean_absolute_error,}METRICS_TO_PLOT=["mse","mae"]# list of featuresMODELS_W_FEATURES=["linear","rf"]FEATURE_LIST=["month_2","month_3","month_4","month_5","month_6","month_7","month_8","month_9","month_10","month_11","month_12",]# a dictionary with the available modelsSELECTED_MODEL="naive"MODEL_DICT={"naive":naive_forecast,"mean":mean_forecast,"linear":linear_model,"rf":rf_model,}

5. 运行回测

在最后一个脚本run_backtest.py中,我们加载数据并根据配置运行回测。

你可能会说,这可能不是为模型生成特征的最佳位置,我完全同意。在这种情况下,因为我们只是添加了月份虚拟变量,所以我将它们留在这里,而不是在单独的步骤中添加。理想情况下,我们应该将这一步骤单独保留,并在添加特征前后对数据集进行版本控制。

importpandasaspdfromsrc.backtesterimportTSBacktesterfromsrc.configimport*fromsrc.model_definitionsimport*fromdvcliveimportLive# load datadf=pd.read_csv("data/time_series.csv",index_col=0)df.index=pd.to_datetime(df.index)# generate featuresdummies=pd.get_dummies(df.index.month,prefix="month",drop_first=True)dummies.index=df.index df=pd.concat([df,dummies],axis=1)# run the backtestbacktester=TSBacktester(MODEL_DICT[SELECTED_MODEL],BT_START_DATE,BT_END_DATE,BACKTEST_FREQ,DATA_FREQ,FCST_HORIZON,rolling_window_size=ROLLING_WINDOW_SIZE,)ifSELECTED_MODELinMODELS_W_FEATURES:backtester.run_backtest(df,target_col="y",verbose=True,features=FEATURE_LIST)else:backtester.run_backtest(df,target_col="y",verbose=True)backtest_metadata,backtest_results=backtester.evaluate_backtest(BT_METRICS,model_name=SELECTED_MODEL)print("Backtest results ----")print(backtest_results)# tracking experiments with DVCwithLive(save_dvc_exp=True)aslive:live.log_params(backtest_metadata)formetric_name,metric_valueinbacktest_results.items():live.log_metric(metric_name,metric_value,plot=False)scores_df=pd.DataFrame(list(backtest_results.items()),columns=["metric","value"])formetricinMETRICS_TO_PLOT:temp_df=scores_df[scores_df["metric"].str.contains(metric)&amp;~scores_df["metric"].str.contains("total")]temp_df["horizon"]=temp_df["metric"].str.split("_").str[-1]live.log_plot(f"backtest_scores_{metric}",temp_df,x="horizon",y="value",template="linear",title=f"Backtest scores -{metric}",y_label="Score",x_label="Horizon",)

为了跟踪回测结果及其元数据,我们做了两件事。

首先,我们确保回测类将所有必要的信息存储在字典中,我们可以使用 DVC 轻松跟踪这些信息。

其次,使用dvcliveLive上下文,我们将回测的元数据及其分数一起记录。对于元数据,我们使用log_params方法。或者,我们可以使用log_param方法单独记录每个参数。在这种情况下,我们将遍历包含元数据的字典,并将参数名称及其值传递给log_param方法。对于性能分数,我们使用log_metric方法迭代跟踪它们。此外,我们将plot标志设置为false,因为我们不希望使用 DVC 绘制这些指标。

在查看结果之前,先提一下dvclive期望每个运行都通过 Git 进行跟踪。这意味着它将每次将每个运行保存到相同的路径,并覆盖结果。我们指定了save_dvc_exp=True来自动跟踪为 DVC 实验。在幕后,DVC 实验是 DVC 可以识别的 Git 提交,但与此同时,它们不会使我们的 Git 历史记录杂乱无章或创建额外的分支。

要实际运行实验,我们更改config.py脚本中的参数值,然后运行run_backtest.py脚本。就这样!在后台,DVC 正在跟踪结果,正如我们将在下一节中看到的。

回测结果

我们可以使用DVC VS Code 扩展来检查实验结果。这是我最喜欢的与 DVC 交互的方式,因为我们不需要离开 IDE。此外,在实验标签页中,我们可以通过隐藏某些列、重新排序或根据特定指标的值对行进行排序来交互式地调整包含结果的表格。或者,我们也可以在终端中显示包含结果的表格。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/abe675d9b4ed2c88c3c2d26a02ace844.png

VS Code 中实验标签页的预览

总共进行了 7 次实验:

  • 前四个都是我们之前定义的模型。对于这些,我们使用了扩展窗口验证。

  • 然后,我们使用了三种模型(线性、随机森林和均值模型)与滚动窗口验证,窗口大小为 365 天。在这个设置中,我们没有使用朴素模型,因为它会导致与扩展窗口相同的预测。

在按总 MSE 排序结果后,我们可以看到随机森林模型与滚动窗口结合表现最佳。

在实验表中,我用星号标记了最佳模型。此外,我还指出我想比较最佳模型和朴素模型误差指标的图表,这可以作为合理的基准。下面,您可以看到比较这两个实验的 MSE 和 MAE 图表。

https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/e211afe51a490a0fd181fd64342a0ca2.png

VS Code 中图表标签页的预览

可能的下一步

如我之前所述,我们回顾了一个简化的回测结果跟踪示例。自然地,我们可以很容易地扩展它。例如,我们可以:

  • 使用例如 MSE 跨度的方法跟踪图表并比较实验。

  • 为我们的模型生成更多特征,并使用 DVC 管道确保我们项目每个步骤的可重复性。

  • 使用params.yaml文件来参数化我们管道中的所有步骤,包括 ML 模型的训练。

总结

在这篇文章中,我们探讨了将 DVC 的实验跟踪框架应用于回测预测模型。由于该框架非常灵活,我们可以存储我们想要的几乎所有参数和/或指标。然后,只需调整我们的回测方法以输出我们想要跟踪的所有值即可。

您可以在这个仓库中找到本文中使用的代码。一如既往,任何建设性的反馈都十分欢迎。您可以通过LinkedIn、Twitter或评论与我联系。我很想知道您是如何处理实验跟踪的,尤其是在时间序列预测的背景下!

您可能还对以下内容感兴趣:

对你的预测模型进行测试:回测指南

时间序列分析中移动平均线的全面指南

时间序列预测中交互项的全面指南


所有图像,除非另有说明,均为作者所有。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/2 7:22:09

数字电路基础知识中逻辑电平标准的详细解析

深入理解数字电路中的逻辑电平&#xff1a;从TTL到LVCMOS的实战解析 在嵌入式系统和数字硬件设计中&#xff0c;有一个看似基础却极易被忽视的关键点—— 逻辑电平标准 。你有没有遇到过这样的情况&#xff1a;MCU明明发了信号&#xff0c;外设却“无动于衷”&#xff1f;或者…

作者头像 李华
网站建设 2026/7/31 5:09:34

实战入门:在电路仿真circuits网页版中构建基本欧姆定律电路

从零开始学电路&#xff1a;用网页仿真器亲手验证欧姆定律 你还记得第一次接触“电压”“电流”这些词时的困惑吗&#xff1f;它们看不见、摸不着&#xff0c;却在每一块电路板里默默流动。老师讲欧姆定律 $ I V/R $ 的时候&#xff0c;公式简单得只有三个字母&#xff0c;但…

作者头像 李华
网站建设 2026/7/31 4:07:31

Multisim仿真对电子工程创新能力培养的作用:一文说清

Multisim仿真如何点燃电子工程的创新火花&#xff1f;一位工程师的教学手记作为一名长期从事电子系统教学与项目指导的高校教师&#xff0c;我常常被学生问到&#xff1a;“老师&#xff0c;为什么我们总要在电脑上‘搭电路’&#xff0c;而不是直接焊板子&#xff1f;”起初我…

作者头像 李华
网站建设 2026/7/31 4:07:32

Go协程与Java虚拟线程:并发编程,谁主沉浮?

基本概念&#xff1a;师出同门却各有千秋Go协程是Go语言原生支持的轻量级线程&#xff0c;由Go运行时直接管理。通过简单的go关键字&#xff0c;你就能轻松启动一个协程&#xff0c;极大地简化了并发编程的复杂度。代码语言&#xff1a;javascriptAI代码解释go func() {fmt.Pri…

作者头像 李华
网站建设 2026/8/3 23:06:47

微pe网络模块加载GLM-TTS云端模型节省本地空间

微pe网络模块加载GLM-TTS云端模型节省本地空间 在教育机构的语音课件制作中心&#xff0c;一台十年前的老式台式机正安静地运行着——没有硬盘、没有GPU&#xff0c;却刚刚完成了一段自然流畅、带有教师本人音色和情感语调的中文朗读音频。这一切的背后&#xff0c;并非魔法&a…

作者头像 李华
网站建设 2026/7/31 8:37:28

GLM-TTS能否用于潜水装备语音提示?水下通信语音预演

GLM-TTS能否用于潜水装备语音提示&#xff1f;水下通信语音预演 在深海作业、科研潜航甚至军事行动中&#xff0c;信息传递的准确性和效率直接关系到人员安全与任务成败。传统的潜水沟通方式——手势、写字板、灯光信号——虽然可靠&#xff0c;但存在表达局限、响应延迟和误读…

作者头像 李华