返回列表

5th place solution

605. Enefit - Predict Energy Behavior of Prosumers | predict-energy-behavior-of-prosumers

开始: 2023-11-02 结束: 2024-04-30 新能源发电 数据算法赛
第5名方案 - 能源预测

第5名方案

关于我的一些话

我在2011年完成了人工智能专业的博士学业,但从那以后,我选择了去旅行和冒险,而不是走研发的道路。直到近几年我才重新回到机器学习领域,尽管关于人工智能的炒作层出不穷,我仍然很难找到一份工作。或许作为一名生活在泰国的外籍人士让情况更复杂,所以我目前在做自由职业,也正因如此我有很多时间可以投入到这次竞赛中。我很享受这次比赛,首先因为它是一堂非常棒的课程,其次因为对硬件要求很低,能够进行大量的实验。

方案概述

在尝试了一些 scikit‑learn 的回归模型后,出于好奇我转向了 XGBoost,随后又转向了 LightGBM,它在开箱即用的情况下就给出了最好的基线。我以前没有在实际项目中使用过梯度提升方法,所以这也更加有趣。我发现学习更经验丰富的选手分享的笔记本非常有效,特别是那些代码编写得非常优雅的例子,例如 Enefit│XGBoost│starterEnefit Generic Notebook(我确信这些是之后许多笔记本的代码来源)。这也是我第一次看到 polars 的使用。感谢这些前辈的指引!

正如大家所知道的,这个比赛主要围绕特征选择和特征工程展开。我进行了大量的实验,可以说整个过程就是一个“思路 & 评估”的大循环。MLflow 在这方面特别有帮助。显然我已经记不清所有尝试过的想法,后文只描述最终保留下来的一些思路。下一节会给出更详细的特征信息。

在模型方面,我选择了 LightGBM,随后主要是对参数进行精调,并最终组成集成模型。参数调优采用了一种简单的自定义进化搜索,并使用了两阶段交叉验证。第一阶段是时间序列交叉验证,采用 3 折,每折保留 2 个月的测试数据,具体如下:

  • 训练数据截至 2022‑09‑01,测试数据截至 2022‑11‑01
  • 训练数据截至 2022‑11‑01,测试数据截至 2023‑01‑01
  • 训练数据截至 2023‑01‑01,测试数据截至 2023‑03‑01

该划分主要用于紧张的算法调参和特征选择。第二阶段使用剩余的 3 个月数据(截至 2023‑05‑26)进行较为温和的手工调优和选择。最终的模型选择自然基于公开测试集(public LB)分数。值得一提的是,我本来想通过公开排行榜的分数来提升自己的排名,但最终只获得了第 22 名。也许本地的交叉验证在一定程度上限制了我的提升,不过我仍然觉得我的方案很幸运。

相较于其他方案,我认为我的方案更为简洁。关键点在于将“产能”和“消费”单元分别处理。产能模型使用 75 个特征,消费模型使用 85 个特征。使用 scikit‑learn 的 VotingRegressor 将 3 个 LightGBM 回归估计器集成在一起,每个部分的参数略有不同,具体如下:

参数产能消费
boosting_typegbdtgbdt
num_leaves358358
max_depth1010
learning_rate0.020.025
min_split_gain0.00.1
min_sum_hessian_in_leaf1.00.1
min_data_in_leaf120360
bagging_fraction1.01.0
bagging_freq00
feature_fraction0.80.8
feature_fraction_bynode0.90.9
lambda_l10.010.01
lambda_l21.01.0
max_bin255255
n_estimators20002000

正如你所看到的,这些数值并没有特别的理论依据,只是通过调参得到的结果。所有的训练数据都用于提交评估,且没有使用验证集来做早停。2000 棵树相较于比如 1200 棵树并没有带来显著提升,但在交叉验证中也没有观察到明显的过拟合,而且因为后续还会有更多数据,所以选择了 2000。

方案的另一个重要部分是线上再训练,即每隔 9 天的评分数据进行再训练,整个评估期间大约会进行 10 次训练。由于训练这两个集成模型比较耗时——使用 GPU 大约需要 20 分钟——因此只在评分期间进行训练非常重要。无论如何,每天都必须收集到达的数据。深入理解提交评估在每个阶段的具体运作方式并避免出现意外错误也是至关重要的。我不知道是否有其他方案因为这一步而出局,但整个过程确实有点滑溜,尤其是在 pandas 与 polars 之间的切换时。我甚至遇到了因为这些优秀库版本不同而导致的错误。不过也许主办方已经为所有通过公开数据评估的方案提供了便利。若测试数据出现缺口,可能会非常麻烦。

特征选择与特征工程

关于特征,我常常思考哪种方式更好:特征降维还是特征冗余。我倾向于在可能的情况下减少特征数量、简化模型,但这个两难问题仍然会出现。也就是说,当某个特征或特征集合在交叉验证中表现几乎不变时,你会保留它还是删除它?冠军方案(by hyd)似乎采取了非常宽松的策略,使用了大量的“滞后”特征,据报道并不是很在意输入的特征到底有哪些,但这样真的更好吗?LightGBM 模型本身具备特征选择的能力,能够轻松处理随机或无用的特征,但与此同时,特征过多也会带来捕捉到「虚假模式」的风险。最终,可能还是要取决于具体的问题和特征本身。

我曾一度使用 Optuna 进行特征选择,但后来更喜欢使用简单的自定义进化搜索,这种方法同样适用于模型的超参数调优。

因为选取的特征数量不算太多,我可以把最终保留下来的特征列在下面,首先是产能模型使用的特征:

county, is_business, product_type, installed_capacity, euros_per_mwh,
hours_ahead_fd, temperature_fd, dewpoint_fd, cloudcover_high_fd, cloudcover_low_fd, cloudcover_mid_fd, cloudcover_total_fd, 10_metre_u_wind_component_fd, 10_metre_v_wind_component_fd, direct_solar_radiation_fd, surface_solar_radiation_downwards_fd, total_precipitation_fd,
temperature_fl, dewpoint_fl, cloudcover_high_fl, cloudcover_low_fl, cloudcover_mid_fl, 10_metre_u_wind_component_fl, 10_metre_v_wind_component_fl, direct_solar_radiation_fl, snowfall_fl, total_precipitation_fl,
temperature_hl, dewpoint_hl, rain_hl, snowfall_hl, surface_pressure_hl, cloudcover_total_hl, cloudcover_low_hl, cloudcover_mid_hl, cloudcover_high_hl, windspeed_10m_hl, winddirection_10m_hl, direct_solar_radiation_hl, diffuse_radiation_hl,
dewpoint_fl_dmean, cloudcover_high_fl_dmean, cloudcover_low_fl_dmean, cloudcover_mid_fl_dmean, cloudcover_total_fl_dmean, 10_metre_u_wind_component_fl_dmean, 10_metre_v_wind_component_fl_dmean, direct_solar_radiation_fl_dmean, surface_solar_radiation_downwards_fl_dmean, snowfall_fl_dmean, total_precipitation_fl_dmean, hours_ahead_fl_dmean, temperature_fl_dmean,
target_6r_2, target_6r_3, target_6r_4, target_6r_5, target_6r_14, day, weekday, sin_dayofyear, cos_dayofyear, cos_hour, sin_hour, target_mean_ic_ratio_2, target_std_6r, target_delta_68_6r, target_7r_2, is_holiday, is_holiday_2, n_holidays_past_week, ssrd_t, ssrd_t_mean

消费模型的特征在上述列表基础上有以下增删(+ 表示增加,- 表示删除):

+ target_2, cloudcover_total_fl, month, surface_solar_radiation_downwards_fl, target_4r_14, snowfall_fd, eic_count, target_delta_68_7r, target_4r_7, target_ratio_18_7r, installed_capacity_ratio, target_7r_7, target_std_7r, hours_ahead_fl, target_mean_2, target_4r_2, shortwave_radiation_hl, target_ratio_68_7r, year

- target_6r_2, target_6r_3, target_6r_5, target_6r_14, cos_dayofyear, ssrd_t, ssrd_t_mean

值得注意的是,天然气价格(gas price)已经从两组特征中删除,同时也不再使用全国范围的历史气象数据。相反,我们引入了大量 *_fl_dmean 特征,这些是“本地预报”气象数据与其过去一周平均值的差值。还有若干 target_* 特征,它们是对“已公开目标值”(revealed targets)进行滞后、比值、平均、差分等变换后保留下来的特征。ssrd_* 特征基于 此处 讨论的变换思路。is_holiday_2 表示“如果两天前是节假日则为真”,这在与最近公开目标值(即两天前的目标值)一起使用时可能会有帮助。

另一个经常出现的难题是日期相关的特征,例如 daymonthyear,它们在交叉验证中往往处于显著性的边缘。拥有对一年中第几天的正弦/余弦变换本应使 daymonth 变得冗余,但实际上并不尽然。最令人困惑的是 year,它的取值范围非常有限。有时会看到建议保留此类特征,但我实在想不通模型如何利用一个在预测时会出现全新未见过取值的特征。或许只是在训练阶段让模型看到不同的年份就已经有帮助了?希望能够找到更合适的解释。

方案的核心之一是“目标变换”。我在这上面做了大量尝试,最终采用了两个简单的变换方式:

对于产能单元:

z = target / (1 + installed_capacity),

其中 installed_capacity 是在预测时可获取的最新值,即滞后 2 天的数据。

对于消费单元:

z = target / (1 + target_avg),

这里的 target_avg 是最近 7 天(例如从 d‑2 到 d‑9)同一时刻的目标值的平均值,同样也是使用 d‑2 的数据进行采样。有趣的是,我曾尝试使用“完美信息”进行训练,即不加入任何滞后,仿佛当前时间段的所有信息都已知,但结果对预测几乎没有影响。还有一些额外的处理和缺失值填充,以避免出现错误。分母中的额外 1 是为了防止在其他项接近零的极端情况下出现极端值(也可以直接做截断)。另外一种我不知道最终是否有帮助的做法是对变换后的目标缺失值进行填充,填充值取自同类型单元在全国范围内的中位数。我认为这可能对预测中出现的意外或中断的单元有帮助,不过根据交叉验证,使用普通常数填充效果同样不错。

以上就是全部内容。非常感谢主办方组织这场精彩的比赛,祝贺获奖者,也向所有参赛者致敬。欢迎留言交流,下次再会!

同比赛其他方案