Datawhale AI夏令营 机器学习Task2 笔记打卡

本次打卡,主要是来按照教程和自己的思考,完成的一次上分之旅。

解决时间序列问题的方法有很多,本次,我主要采用的是机器学习中的Boosting提升树家族的模型。按照官方教程,模型皆是基于梯度提升决策树(GBDT)作为基学习器的集成学习模型。在各大机器学习领域的比赛中使用量最大,效果普遍较好的便是极端梯度提升树(XGBoost)、轻量梯度提升机(LightGBM)以及CatBoost。

本次笔记,我将分为选择模型、调参、特征工程三个部分来尝试。

我们暂时根据官方教程完成特征工程:

# 合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)

# 历史平移
for i in range(10,30):
    data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
    
# 窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3

# 进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)

# 确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]

这段特征工程主要实现了三点:

(1)将数据合并,并按照 id 和时间 dt 进行排序,确保数据按时间降序排列;

(2)历史平移:对每个 id 组进行平移操作,将 target 列进行10到30步的历史平移,生成新的特征列;

(3)窗口平移:计算最近三次历史平移的目标值的均值,并生成一个新的特征。

必须引起关注的是,特征工程将直接决定预测结果的好坏,这也是我打本次比赛总结的经验教训之一,后文会再说道的。

完成了特征工程后,我们分别采取三个集成学习模型进行训练:

# XGBoost
import pandas as pd
import numpy as np
import xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# 加载处理后的数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\train.csvv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\test.csv')

# 准备训练数据和标签
X = train_df.drop(columns=['target', 'id', 'dt'])
y = train_df['target']
X_test = test_df.drop(columns=['id', 'dt'])

# 拆分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建XGBoost DMatrix
train_data = xgb.DMatrix(X_train, label=y_train)
val_data = xgb.DMatrix(X_val, label=y_val)
test_data = xgb.DMatrix(X_test)

# 设置参数
params = {
    'objective': 'reg:squarederror',
    'eval_metric': 'rmse',
    'eta': 0.05,
    'max_depth': 6,
    'subsample': 0.9,
    'colsample_bytree': 0.9,
    'alpha': 0.1,
    'lambda': 1.0,
}

# 设置回调函数
callbacks = [
    xgb.callback.EarlyStopping(rounds=500, save_best=True),
    xgb.callback.EvaluationMonitor(period=100)
]

# 训练模型
model = xgb.train(params, train_data, num_boost_round=50000, evals=[(train_data, 'train'), (val_data, 'eval')],
                  callbacks=callbacks)

# 进行预测
predictions = model.predict(test_data)

# 创建预测结果数据框
results_df = test_df[['id', 'dt']].copy()
results_df['target'] = predictions

# 保存预测结果
results_df.to_csv('XGB_submit.csv', index=False)

print("模型训练和预测完成,预测结果已保存。")

XGBoost最终得分为264.79分。

# CatBoost
import pandas as pd
import numpy as np
from catboost import CatBoostRegressor, Pool
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# 加载处理后的数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\train.csv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\test.csv')

# 准备训练数据和标签
X = train_df.drop(columns=['target', 'id', 'dt'])
y = train_df['target']
X_test = test_df.drop(columns=['id', 'dt'])

# 拆分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建CatBoost Pool
train_pool = Pool(X_train, y_train)
val_pool = Pool(X_val, y_val)
test_pool = Pool(X_test)

# 设置参数
params = {
    'iterations': 50000,
    'learning_rate': 0.05,
    'depth': 6,
    'loss_function': 'RMSE',
    'eval_metric': 'RMSE',
    'random_seed': 42,
    'logging_level': 'Verbose',
    'od_type': 'Iter',
    'od_wait': 500  # 提前停止轮数
}

# 训练模型
model = CatBoostRegressor(**params)
model.fit(train_pool, eval_set=val_pool, verbose=100, early_stopping_rounds=500)

# 进行预测
predictions = model.predict(test_pool)

# 创建预测结果数据框
results_df = test_df[['id', 'dt']].copy()
results_df['target'] = predictions

# 保存预测结果
results_df.to_csv('C_submit.csv', index=False, float_format='%.6f')

print("模型训练和预测完成,预测结果已保存。")

CatBoost最终得分为276.98分

# LightGBM
def time_model(lgb, train_df, test_df, cols):
    # 训练集和验证集切分
    trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target']
    val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target']
    # 构建模型输入数据
    train_matrix = lgb.Dataset(trn_x, label=trn_y)
    valid_matrix = lgb.Dataset(val_x, label=val_y)
    # lightgbm参数
    lgb_params = {
        'boosting_type': 'gbdt',
        'objective': 'regression',
        'metric': 'mse',
        'min_child_weight': 5,
        'num_leaves': 2 ** 5,
        'lambda_l2': 10,
        'feature_fraction': 0.8,
        'bagging_fraction': 0.8,
        'bagging_freq': 4,
        'learning_rate': 0.05,
        'seed': 2024,
        'nthread' : 16,
        'verbose' : -1,
    }
    # 训练模型
    
    callbacks = [log_evaluation(period=100), early_stopping(stopping_rounds=500)]

    model = lgb.train(lgb_params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], 
                      categorical_feature=[],callbacks=callbacks)
    # 验证集和测试集结果预测
    val_pred = model.predict(val_x, num_iteration=model.best_iteration)
    test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
    # 离线分数评估
    score = mean_squared_error(val_pred, val_y)
    print(score)
       
    return val_pred, test_pred
    
lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)

# 保存结果文件到本地
test['target'] = lgb_test
test[['id','dt','target']].to_csv('submit1.csv', index=None)

LightGBM的最终得分为259.97分。

由此可见,LightGBM更胜一筹。

这里顺带提一嘴LightGBM,可以看成是 XGBoost 的升级加强版本,2017 年经微软推出后,便成为各种数据竞赛中刷分拿奖的神兵利器。和 XGBoost 相比,其在大规模数据集上跑起来更加轻盈。 与XGBoost对比以下几点:

(1)模型精度:XGBoost LightGBM 相当;

(2)训练速度:LightGBM 远快于 XGBoost

(3)内存消耗:LightGBM 远小于 XGBoost

(4)缺失值特征:XGBoost LightGBM 都可以自动处理特征缺失值;

(5)分类特征:XGBoost不支持类别特征,需要 OneHot编码预处理,LightGBM 直接支持类别特征。

我们可以用以下公式来浅显的揭示二者之间的关系:
LightGBM = XGBoost + Histogram + GOSS + EFB

对机器学习原理感兴趣的同学也可以来看看我写的机器学习方法课堂系列,链接放在这里了,欢迎前来交流!https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzk0ODI1MzMwNg==&action=getalbum&album_id=3315279070611619844#wechat_redirect

接下来,我就尝试基于LightGBM的基础上,进行了优化思考。我当时的思考方向无非就两个,要么继续思考特征工程,要么就尝试调参呗。我先开始尝试的是调参,调参的两大基本方法便是网格搜索法和贝叶斯优化。我试着动手做了做贝叶斯优化:

# Bayes优化
# 定义贝叶斯优化对象
lgb_bo = BayesianOptimization(
    lgb_cv,
    {
        'num_leaves': (30, 100),
        'max_depth': (5, 15),
        'learning_rate': (0.05, 0.2),
        'min_child_samples': (20, 100),
    },
    random_state=2024
)

# 开始优化
lgb_bo.maximize(init_points=5, n_iter=10)

# 获取最佳参数
best_params = lgb_bo.max['params']
print("Best Params:", best_params)

# 使用最佳参数重新训练模型,并预测测试集
best_params['num_leaves'] = int(best_params['num_leaves'])
best_params['max_depth'] = int(best_params['max_depth'])
best_params['min_child_samples'] = int(best_params['min_child_samples'])

lgb_model = lgb.LGBMRegressor(**best_params)
lgb_oof, lgb_test = cv_model(lgb, train[train_cols], train['target'], test[train_cols], 'lgb')

# 保存输出结果
final_test = lgb_test
submission = pd.DataFrame({'id': test['id'], 'dt': test['dt'], 'target': final_test})
submission.to_csv('Bayes_LightGBM_submit4.csv', index=False)

首先,我定义了贝叶斯优化搜索最佳参数的范围(即在我定的范围里,找到一个最合适的参数):

num_leaves: 30到100之间。

max_depth: 5到15之间。

learning_rate: 0.05到0.2之间。

min_child_samples: 20到100之间。

随机种子随便设一个,今年2024年,那就2024吧哈哈哈。然后,启动贝叶斯优化器,迭代10次。

上图是我的代码输出日志,详细的反映了每一次迭代贝叶斯优化的过程。最终,得出最佳的参数组合如下:Best Params: {'learning_rate': 0.2, 'max_depth': 12.753964325020366, 'min_child_samples': 20.0, 'num_leaves': 86.1360835609615}。

learning_rate: 0.2

max_depth: 12.75(向下取整为12)

min_child_samples: 20

num_leaves: 86

然后,我将上述参数组合用在了LightGBM中,确实上了一点分,达到了251.22分。嗯,不错,算是努力没有白费,这也算是日拱一卒,功不唐捐嘛。

继续,我就开始走上了另外一条路,那就是做特征工程。请原谅我说这条路实在是走的很费劲,因为本人之前基本没有特征工程很成功的经验。

针对时间序列问题,我查阅了相关资料,一般的特征工程有以下几种:

(1)日期特征:考虑更丰富的日期特征,例如月、季度等;

(2)移动平均:计算更长时间范围内的移动平均;

(3)差分特征:计算电力消耗的差分;

(4)交互特征:构建不同特征之间的交互特征;

(5)聚合特征:对每个房屋类型计算全局的统计量,例如均值、中位数、标准差等;

(6)周期特征:一般采用傅里叶。

基于此,我进行了以下尝试:

尝试一:处理日期特征、创建时间序列特征、创建滚动统计特征

# 特征工程尝试一

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

# 合并训练和测试数据集以便于特征工程
train_df['is_train'] = 1
test_df['is_train'] = 0
test_df['target'] = np.nan
full_df = pd.concat([train_df, test_df], axis=0, ignore_index=True)

# 处理日期特征
full_df['day_of_week'] = full_df['dt'] % 7  # 假设1代表周一
full_df['is_weekend'] = full_df['day_of_week'].isin([5, 6]).astype(int)

# 处理房屋类型
le = LabelEncoder()
full_df['type'] = le.fit_transform(full_df['type'])

# 创建时间序列特征
def create_lag_features(df, lags, col):
    for lag in lags:
        df[f'{col}_lag_{lag}'] = df.groupby('id')[col].shift(lag)
    return df

lags = [1, 2, 3, 7, 14, 30]
full_df = create_lag_features(full_df, lags, 'target')

# 创建滚动统计特征
def create_rolling_features(df, windows, col):
    for window in windows:
        df[f'{col}_roll_mean_{window}'] = df.groupby('id')[col].shift(1).rolling(window).mean()
        df[f'{col}_roll_max_{window}'] = df.groupby('id')[col].shift(1).rolling(window).max()
        df[f'{col}_roll_min_{window}'] = df.groupby('id')[col].shift(1).rolling(window).min()
        df[f'{col}_roll_std_{window}'] = df.groupby('id')[col].shift(1).rolling(window).std()
    return df

windows = [3, 7, 14, 30]
full_df = create_rolling_features(full_df, windows, 'target')

# 填充缺失值
full_df.fillna(0, inplace=True)

# 拆分回训练和测试集
train_df = full_df[full_df['is_train'] == 1].drop(['is_train'], axis=1)
test_df = full_df[full_df['is_train'] == 0].drop(['is_train', 'target'], axis=1)

# 保存处理后的数据集
train_df.to_csv('new_train.csv', index=False)
test_df.to_csv('new_test.csv', index=False)

print("特征工程完成,处理后的数据集已保存。")

# 加载处理后的数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\new_train.csv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\new_test.csv')

# 准备训练数据和标签
X = train_df.drop(columns=['target', 'id', 'dt'])
y = train_df['target']
X_test = test_df.drop(columns=['id', 'dt'])

# 拆分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建LightGBM数据集
train_data = lgb.Dataset(X_train, label=y_train)
val_data = lgb.Dataset(X_val, label=y_val, reference=train_data)

# 设置参数
params = {
    'objective': 'regression',
    'metric': 'mse',
    'boosting_type': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.9
}

# 训练模型
# 设置回调函数
callbacks = [
    log_evaluation(period=100),  # 每100轮记录一次日志
    early_stopping(stopping_rounds=500)  # 500轮没有提升时提前停止
]

# 训练模型
model = lgb.train(params, train_data, num_boost_round=50000, valid_sets=[train_data, val_data],
                  callbacks=callbacks)

# 进行预测
predictions = model.predict(X_test, num_iteration=model.best_iteration)

# 创建预测结果数据框
results_df = test_df[['id', 'dt']].copy()
results_df['target'] = predictions

# 保存预测结果
results_df.to_csv('Newnew_L_submit5.csv', index=False)

print("模型训练和预测完成,预测结果已保存。")

效果惊人的差,分数达到了四位数,我真的裂开。

不过不能就此放弃,我继续进行了尝试二:在尝试一的基础上,我增加了创建移动平均特征,在不同窗口大小(7天、30天、60天、90天)下的移动平均值;以及创建差分特征,一个名为create_diff_features的函数被定义来计算目标变量target在不同滞后阶数(1、2、3、7、14、30天)下的差分。

# 特征工程尝试二
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from lightgbm import early_stopping, log_evaluation

# 加载数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\train.csv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\test.csv')

# 合并训练和测试数据集以便于特征工程
train_df['is_train'] = 1
test_df['is_train'] = 0
test_df['target'] = np.nan
full_df = pd.concat([train_df, test_df], axis=0, ignore_index=True)

# 处理日期特征
full_df['day_of_week'] = full_df['dt'] % 7  # 假设1代表周一
full_df['is_weekend'] = full_df['day_of_week'].isin([5, 6]).astype(int)
full_df['month'] = (full_df['dt'] // 30) % 12  # 假设每月30天
full_df['quarter'] = (full_df['dt'] // 90) % 4  # 假设每季度90天

# 处理房屋类型
le = LabelEncoder()
full_df['type'] = le.fit_transform(full_df['type'])

# 创建移动平均特征
def create_moving_average_features(df, windows, col):
    for window in windows:
        df[f'{col}_ma_{window}'] = df.groupby('id')[col].transform(lambda x: x.rolling(window).mean())
    return df

windows = [7, 30, 60, 90]
full_df = create_moving_average_features(full_df, windows, 'target')

# 创建差分特征
def create_diff_features(df, lags, col):
    for lag in lags:
        df[f'{col}_diff_{lag}'] = df.groupby('id')[col].diff(lag)
    return df

lags = [1, 2, 3, 7, 14, 30]
full_df = create_diff_features(full_df, lags, 'target')

# 填充缺失值
full_df.fillna(0, inplace=True)

# 拆分回训练和测试集
train_df = full_df[full_df['is_train'] == 1].drop(['is_train'], axis=1)
test_df = full_df[full_df['is_train'] == 0].drop(['is_train', 'target'], axis=1)

# 保存处理后的数据集
train_df.to_csv('processed_train.csv', index=False)
test_df.to_csv('processed_test.csv', index=False)

print("特征工程完成,处理后的数据集已保存。")

结果比上次的好了点,但是也有1111.12分。到这里,我突然意识到了一个问题,特征工程并不是各种花里胡哨的堆砌,简单的也许能直击要害。于是,我逐步将上述特征工程进行了删减,并去除了处理房屋类型这致命的一步(我后来才发现,这一步会将原有的id打乱,最终不好恢复,直接影响了最终提交的csv文件,造成大面积错误。这也是分数为何居高不下的原因之一。我再进行慢慢的尝试,分数在逐步下降,八百、六百、五百到298.63分就下不去了。这时,我决定换方法。

这就有了尝试三:主要构建了历史平移特征、差分特征、和窗口统计特征

# 尝试三
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

# 合并训练数据和测试数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\train.csv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\test.csv')
data = pd.concat([train_df, test_df], axis=0).reset_index(drop=True)
data = data.sort_values(['id', 'dt'], ascending=False).reset_index(drop=True)

# 历史平移特征
for i in range(10, 51, 5):
    data[f'target_shift{i}'] = data.groupby('id')['target'].shift(i)

# 历史平移 + 差分特征
for i in range(1, 4):
    for j in range(10, 51, 5):
        data[f'target_shift{j}_diff{i}'] = data.groupby('id')[f'target_shift{j}'].diff(i)

# 增加二阶差分特征
for i in range(10, 51, 5):
    data[f'target_shift{i}_diff2'] = data.groupby('id')[f'target_shift{i}'].diff(2)

# 窗口统计特征
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]:
    for j in range(10, 51, 5):
        data[f'target_shift{j}_win{win}_mean'] = data.groupby('id')[f'target_shift{j}'].rolling(window=win, min_periods=3, closed='left').mean().values
        data[f'target_shift{j}_win{win}_max'] = data.groupby('id')[f'target_shift{j}'].rolling(window=win, min_periods=3, closed='left').max().values
        data[f'target_shift{j}_win{win}_min'] = data.groupby('id')[f'target_shift{j}'].rolling(window=win, min_periods=3, closed='left').min().values
        data[f'target_shift{j}_win{win}_std'] = data.groupby('id')[f'target_shift{j}'].rolling(window=win, min_periods=3, closed='left').std().values

# 填充缺失值
data.fillna(0, inplace=True)

# 拆分回训练和测试集
train_df = data[data['target'].notna()]
test_df = data[data['target'].isna()].drop(['target'], axis=1)

# 保存处理后的数据集
train_df.to_csv('1_train_optimized.csv', index=False)
test_df.to_csv('1_test_optimized.csv', index=False)
print("优化后的特征工程完成,处理后的数据集已保存。")

# LightGBM训练和预测代码
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from lightgbm import early_stopping, log_evaluation

# 加载数据
train_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\train.csv')
test_df = pd.read_csv(r'D:\数学建模竞赛\2024DW夏令营2机器学习\dataset\test.csv')

# 准备训练数据和标签
X = train_df.drop(columns=['target', 'id', 'dt'])
y = train_df['target']
X_test = test_df.drop(columns=['id', 'dt'])

# 拆分训练集和验证集
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)

# 创建LightGBM数据集
train_data = lgb.Dataset(X_train, label=y_train)
val_data = lgb.Dataset(X_val, label=y_val, reference=train_data)

# 设置参数
params = {
    'objective': 'regression',
    'metric': 'mse',
    'boosting_type': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.9
}

# 设置回调函数
callbacks = [
    log_evaluation(period=100),  # 每100轮记录一次日志
    early_stopping(stopping_rounds=500)  # 500轮没有提升时提前停止
]

# 训练模型
model = lgb.train(params, train_data, num_boost_round=50000, valid_sets=[train_data, val_data],
                  callbacks=callbacks)

# 进行预测
predictions = model.predict(X_test, num_iteration=model.best_iteration)

# 创建预测结果数据框
results_df = test_df[['id', 'dt']].copy()
results_df['target'] = predictions

# 保存预测结果
results_df.to_csv('submit7.csv', index=False)

print("模型训练和预测完成,预测结果已保存。")

(1)历史平移特征:通过历史平移获取上个阶段的信息;

(2)差分特征:可以帮助获取相邻阶段的增长差异,描述数据的涨减变化情况。在此基础上还可以构建相邻数据比值变化、二阶差分等;

(3)窗口统计特征:窗口统计可以构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。

这个尝试应该是我最英明的尝试了,达到了233.33分(历史最高分,大赛30名左右)。

本次笔记就到此为止了,也算是记录了我这三天来的所有学习和尝试吧。过程很辛苦,也很有趣,感谢Datawhale这个用心的开源组织!我们第三期见!

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值