这一期学习进阶的特征提取与分析,构建深度学习方案,拿下更高分数,冲冲冲。
前排提示:本篇笔记被评为优秀笔记,感谢支持
项目链接:Task3:尝试使用深度学习方案 - 飞书云文档 (feishu.cn)
前两期介绍了代码,没有仔细深化每个优化方向,这里进行补充并尝试新方案。下面是流程图:
之前介绍过优化方向 ,开始实践。
特征优化
增加历史平移。
# 历史平移
for i in range(10,36):
data[f'target_shift{i}'] = data.groupby('id')['target'].shift(i)
创建从 10 到 35 的滞后特征(共 26 个滞后特征),每个都是将 target
列平移 i
个时间步,从而获取上个阶段的信息,效果如下:
使用差分特征。
# 差分特征
for i in range(1,4):
data[f'target_shift10_diff{i}'] = data.groupby('id')['target_shift10'].diff(i)
计算序列中相邻时间步之间的差异,即当前行和前 i
行的差值,对target_shift10
列进行操作,效果如下:
增加窗口统计。
# 窗口统计
for win in [15,30,50,70]:
data[f'target_win{win}_mean'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_win{win}_max'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_win{win}_min'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_win{win}_std'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').std().values
# 历史平移 + 窗口统计
for win in [7,14,28,35,50,70]:
data[f'target_shift10_win{win}_mean'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_shift10_win{win}_max'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_shift10_win{win}_min'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_shift10_win{win}_sum'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').sum().values
data[f'target_shift710win{win}_std'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').std().values
构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。效果也是增加几列,不展示了。
模型融合
顾名思义,就是将多个模型的结果进行融合,最常见的就是加权求和。这里选择使用 lightgbm、xgboost 和 catboost 模型,依次跑完这三个模型,然后将三个模型的结果进行 取平均融合。
对于每个模型均选择经典的 K 折交叉验证方法进行离线评估,大体流程如下:
1、K 折交叉验证会把样本数据随机的分成K份;
2、每次随机的选择 K-1 份作为训练集,剩下的 1 份做验证集;
3、当这一轮完成后,重新随机选择 K-1 份来训练数据;
4、最后将 K 折预测结果取平均作为最终提交结果。
# 选择lightgbm模型
lgb_oof, lgb_test = cv_model(lgb, train[train_cols], train['target'], test[train_cols], 'lgb')
# 选择xgboost模型
xgb_oof, xgb_test = cv_model(xgb, train[train_cols], train['target'], test[train_cols], 'xgb')
# 选择catboost模型
cat_oof, cat_test = cv_model(CatBoostRegressor, train[train_cols], train['target'], test[train_cols], 'cat')
# 进行取平均融合
final_test = (lgb_test + xgb_test + cat_test) / 3
需要注意的地方就是它和 Task2 一样,必须修改 lightgbm 模型的参数,不要修改另外两个模型参数!!我以为都要调整一致,结果运行到最后才报错,千万小心,源代码只需要改一处即可。
代码的逻辑很清晰,这里就不解释了,右键运行!
加权平均融合的效果可能一般,另外一种就是 stacking 融合,stacking 是一种分层模型集成框架。以两层为例,第一层由多个基学习器组成,其输入为原始训练集,第二层的模型则是以第一层基学习器的输出作为特征加入训练集进行再训练,从而得到完整的 stacking 模型。
第一层:(类比cv_model函数)
-
划分训练数据为 K 折(5 折为例,每次选择其中四份作为训练集,一份作为验证集);
-
针对各个模型进行 随机森林 RF、极端随机树 ET、梯度提升决策树 GBDT、极限梯度提升 XGB,分别进行 5 次训练,每次训练保留一份样本用作训练时的验证,训练完成后分别对 Validation set,Test set 进行预测,对于 Test set 一个模型会对应 5 个预测结果,将这 5 个结果取平均;对于 Validation set 一个模型经过 5 次交叉验证后,所有验证集数据都含有一个标签。此步骤结束后:5 个验证集(总数相当于训练集全部)在每个模型下分别有一个预测标签,每行数据共有 4 个标签(4 个算法模型),测试集每行数据也拥有四个标签(4 个模型分别预测得到的)
第二层:(类比stack_model函数)
将训练集中的四个标签外加真实标签当作 五列新的特征作为新的训练集,选取一个训练模型,根据新的训练集进行训练,然后应用 测试集的四个标签组成的测试集 进行预测作为最终的result。
本质上,这种方法是在上一种融合的基础上增加了新的模型(下面选择 Ridge 回归模型),再次进行训练并融合。
上面已经讲解过原理了,这里只放关键部分,不再赘述。
显然,进一步的处理得到了更好的结果。后面介绍优化方向。
深度学习方案
深度学习的流程其实挺标准的,都是 读数据、预处理、选择网络架构、评估。
这里给出的代码是残缺的(疯狂报错),只有一个框架,具体细节还需要自己填充,反复试错,终于调试成功。
不要用默认的 keras,安装TensorFlow,我选择的是最新版。
导包以及后续的方法使用,全部进行更新。
from keras.src.optimizers import Adam
import tensorflow as tf
# 定义模型
def build_model(look_back, n_features, n_output):
model = tf.keras.models.Sequential()
model.add(tf.keras.layers.LSTM(50, input_shape=(look_back, n_features)))
model.add(tf.keras.layers.RepeatVector(n_output))
model.add(tf.keras.layers.LSTM(50, return_sequences=True))
model.add(tf.keras.layers.TimeDistributed(tf.keras.layers.Dense(1)))
model.compile(loss='mean_squared_error', optimizer=Adam(0.001))
return model
更新数据预处理过程,让它的 id 与数值进行对应,修改为下面的:
# 准备测试数据集
OOT = []
OOT_ids = []
for id, data in datasets.items():
a = data[:100, 3] # 电力数据在第3列
a = np.append(a, np.array([0] * (100 - len(a))))
OOT.append(a[::-1])
OOT_ids.append(data[:10, 0]) # id在第0列
数据结果的保存,结构的规整如下:
# 将预测结果与id和dt结合
result_df = pd.DataFrame({
'id': list(OOT_ids.flatten().tolist()),
'dt': list([10,9,8,7,6,5,4,3,2,1] * 5832),
# 使用 itertools.chain 展平三维列表
'target': list(chain.from_iterable(chain.from_iterable(predicted_values)))
})
# 展示结果
print(result_df.head())
result_df[['id', 'dt', 'target']].to_csv('submit.csv', index=None)
提交官网,看看结果。
这分数出来的时候把我整郁闷了,虽然知道会很低,但是没想到这么低。原因:训练的结果与 id 不匹配,我在最后手动添加,不能确保这一天的数据与这个结果对应。为什么?因为整个训练就用不到其他信息,给定代码中的数据 shape 是 (5832,100) 格式,就没有将 id 与结果关联,是一个巨大的错误。想要改进的话,必须弄清楚两者之间的一一对应关系。我尝试了很久,没成功,后续再尝试。
还有一个原因是深度学习的模型本就对这种时间序列问题不太对口,很难训练出高分,还是尝试优化 baseline 更容易出结果。
优化方向
- 在模型融合时,第二层的模型选择是 Ridge,显然可以更换为更高性能的模型
- 模型参数的选择是固定的,应该仿照 Task2 去寻找最优参数,然后进行筛选
- 同理,特征工程的特征也可以增多,不拘泥于那几个窗口统计
- 模型集成不一定非选择上面的三个,可以更换,提供泛化能力
- 深度学习方案中,重中之重就是将 id 加入训练或同步
- 而且不应该切割前 100 行去训练,增加数据的范围
- 预处理的每个 id 只分配了 5 个时间序列,可以扩展
- 每个时间点的特征数只有 1 个,可以增加
- 模型选择的是 LSTM,明显可以更换再尝试
- 同理,参数调优,增加细节,调整粒度。。。
如果 3 个模型不选择变化时,可以保存结果,不然每次都跑重复的流程,浪费时间(大概半小时 ~ 1小时)
import os
import pickle
import pandas as pd
def save_results(filename, data):
with open(filename, 'wb') as file:
pickle.dump(data, file)
def load_results(filename):
if os.path.exists(filename):
with open(filename, 'rb') as file:
return pickle.load(file)
else:
return None
# 定义文件路径
xgb_oof_file = 'xgb_oof.pkl'
xgb_test_file = 'xgb_test.pkl'
cat_oof_file = 'cat_oof.pkl'
cat_test_file = 'cat_test.pkl'
lgb_oof_file = 'lgb_oof.pkl'
lgb_test_file = 'lgb_test.pkl'
# 选择模型和保存结果
def get_model_results():
# 先尝试加载已保存的结果
xgb_oof = load_results(xgb_oof_file)
xgb_test = load_results(xgb_test_file)
cat_oof = load_results(cat_oof_file)
cat_test = load_results(cat_test_file)
lgb_oof = load_results(lgb_oof_file)
lgb_test = load_results(lgb_test_file)
# 如果结果不存在,则运行模型并保存结果
if xgb_oof is None or xgb_test is None or cat_oof is None or cat_test is None or lgb_oof is None or lgb_test is None:
xgb_oof, xgb_test = cv_model(xgb, train[train_cols], train['target'], test[train_cols], 'xgb')
cat_oof, cat_test = cv_model(CatBoostRegressor, train[train_cols], train['target'], test[train_cols], 'cat')
lgb_oof, lgb_test = cv_model(lgb, train[train_cols], train['target'], test[train_cols], 'lgb')
# 保存结果
save_results(xgb_oof_file, xgb_oof)
save_results(xgb_test_file, xgb_test)
save_results(cat_oof_file, cat_oof)
save_results(cat_test_file, cat_test)
save_results(lgb_oof_file, lgb_oof)
save_results(lgb_test_file, lgb_test)
return xgb_oof, xgb_test, cat_oof, cat_test, lgb_oof, lgb_test
# 调用函数获取结果
xgb_oof, xgb_test, cat_oof, cat_test, lgb_oof, lgb_test = get_model_results()
(1)将第二层的模型更换为 线性回归模型 LinearRegression
分数没有变化,说明这种方法产生的效果很薄弱,最好采取其他模型或方案。
我尝试更换为 随机森林 RandomForestRegressor,却卡死了好久,估计是因为数据量太大导致。换成 n_jobs=1 也没有解决,尝试别的方案吧。
(2)修改模型参数,找到最优
增大训练轮数,修改参数后,发现分数降低了,说明参数没有修改得当,加范围多次尝试吧。
(3)单纯增加训练轮数,这里加到 2000
可以看到,效果并不明显,而且花费的时间很长,所以重心更应该放在参数调优以及模型选择上
(4)继续尝试,更换模型或改进模型