机器学习暑期笔记02--Baseline精读+优化

105 阅读10分钟
书接上回:https://juejin.cn/post/7258483700816314428

最新的竞赛分数:6.38269

tup.png

Baseline精读

先来回顾预测类赛事的主要流程

  • 准备数据
  • 数据预处理
  • 数据可视化
  • 特征工程
  • 训练模型
  • (可视化模型效果/模型融合)
  • 生成最后的预测结果(导出csv文件)

接下来,咱们深入探讨一下baseline的代码,以及鱼佬的优化和建议(鱼佬真的tql!)

一、 导入相关库

本次预测任务baseline使用到的库有: pandas、lightgbm、sklearn、tqdm

  • pandas: Pandas是基于NumPy的Python 库,广泛应用于导入/导出数据、数据分析,以及数据清洗和准备等工作。基本上我们机器学习的任务都是使用pandas导入csv文件,读入的数据保存为dataframe格式(类似于表格)。

  • lightgbm: lightgbm是使用基于学习算法的决策树模型,是一个梯度boosting框架。具有高效,低内存占用的有点。梯度提升决策树(GBDT)是一种流行的机器学习算法,XGBoost和LightGBM都属于这种算法的实现模型。

  • sklearn: 全称为scikit-learn,是python中的机器学习库,建立在numpy、scipy、matplotlib等数据科学包的基础之上,涵盖了机器学习中的样例数据、数据预处理、模型验证、特征选择、分类、回归、聚类、降维等几乎所有环节,功能十分强大。

  • tqdm: python中的进度条库,通过装饰任何可迭代的对象,使代码中的循环(loop)在运行过程中为用户展示进度条。在机器学习训练模型的过程中,进度条还是挺重要的,可以看到模型的当前训练时间和剩余训练时间。

代码:

import pandas as pd # 用于处理数据的工具 
import lightgbm as lgb # 机器学习模型 
LightGBM from sklearn.metrics import mean_absolute_error # 评分 MAE 的计算函数 from sklearn.model_selection import train_test_split # 拆分训练集与验证集工具 
from tqdm import tqdm # 显示循环的进度条工具

二、数据准备

使用pandas库导入数据,准备好待提交的数据格式,不同竞赛有不同的提交格式。 其中,submit为空的DataFrame类型,后续可以非常方便的导出为csv文件。 MAE_scores用于存储本赛题的后续模型的评分。

# 数据准备 train_dataset = pd.read_csv("./data/train.csv") # 原始训练数据。 test_dataset = pd.read_csv("./data/test.csv") # 原始测试数据(用于提交)。 
submit = pd.DataFrame() # 定义提交的最终数据。 
submit["序号"] = test_dataset["序号"] # 对齐测试数据的序号。 
MAE_scores = dict() # 定义评分项。

设置需要预测的标签 (上下部温度),利用了python切片的语法特性。 将数据集进行拆分,分为训练集和测试集。

pred_labels = list(train_dataset.columns[-34:]) # 需要预测的标签。 train_set, valid_set = train_test_split(train_dataset, test_size=0.2) # 拆分数据集。

设置lightgbm模型的参数(大佬给的参数,一般不需要再使用网格调参啥的了),同时禁用了数据日志输出,让代码运行后看起来不那么繁杂。

lgb_params = { 'boosting_type': 'gbdt', ... } // 省略具体参数 太长了

no_info = lgb.callback.log_evaluation(period=-1) # 禁用训练日志输出。

三、特征工程

在进行特征工程时,将输入的数据中的时间特征进行提取和转换,从而使得原始数据更加适用于时间序列分析或者其他机器学习模型的训练。 输入为需要提取时间特征的数据,其中pred_labels为需要预测的标签的列表,如果是测试集,不需要填入。输出为提取时间特征后的数据。

# 时间特征函数
def time_feature(data: pd.DataFrame, pred_labels: list=None) -> pd.DataFrame:
    
    data = data.copy() # 复制数据,避免后续影响原始数据。
    data = data.drop(columns=["序号"]) # 去掉”序号“特征。
    
    data["时间"] = pd.to_datetime(data["时间"]) # 将”时间“特征的文本内容转换为 Pandas 可处理的格式。
    data["month"] = data["时间"].dt.month # 添加新特征“month”,代表”当前月份“。
    data["day"] = data["时间"].dt.day # 添加新特征“day”,代表”当前日期“。
    data["hour"] = data["时间"].dt.hour # 添加新特征“hour”,代表”当前小时“。
    data["minute"] = data["时间"].dt.minute # 添加新特征“minute”,代表”当前分钟“。
    data["weekofyear"] = data["时间"].dt.isocalendar().week.astype(int) # 添加新特征“weekofyear”,代表”当年第几周“,并转换成 int,否则 LightGBM 无法处理。
    data["dayofyear"] = data["时间"].dt.dayofyear # 添加新特征“dayofyear”,代表”当年第几日“。
    data["dayofweek"] = data["时间"].dt.dayofweek # 添加新特征“dayofweek”,代表”当周第几日“。
    data["is_weekend"] = data["时间"].dt.dayofweek // 6 # 添加新特征“is_weekend”,代表”是否是周末“,1 代表是周末,0 代表不是周末。

    data = data.drop(columns=["时间"]) # LightGBM 无法处理这个特征,它已体现在其他特征中,故丢弃。

    if pred_labels: # 如果提供了 pred_labels 参数,则执行该代码块。
        data = data.drop(columns=[*pred_labels]) # 去掉所有待预测的标签。
    
    return data # 返回最后处理的数据。

test_features = time_feature(test_dataset) # 处理测试集的时间特征,无需 pred_labels。
test_features.head(5)

四、模型训练与预测

使用LightGBM模型对数据集待预测的标签进行训练和预测。

总体思路是:

  1. 循环遍历每一个待预测的标签pred_label(这些标签存储在pred_labels列表中)。
  2. 对训练集train_set和验证集valid_set进行时间特征处理time_feature函数对时间特征进行处理,提取月份、日期、小时等。处理后的特征存储在train_featuresvalid_features中。
  3. train_setvalid_set中获取当前预测标签pred_label的标签数据,即训练集标签train_labels和验证集标签valid_labels
  4. 使用lgb.Dataset将处理后的训练特征train_features和相应的训练标签train_labels组合成LightGBM可处理的数据类型train_data
  5. 同理将验证特征valid_features和相应的验证标签valid_labels组合成valid_data
  6. 使用lgb.train函数来训练LightGBM模型。其中,lgb_params是LightGBM模型的参数字典,控制模型的超参数配置。
  7. 使用训练好的模型对验证集和测试集进行预测,验证集的预测结果为valid_predtest_pred
  8. 计算验证集预测结果valid_pred和真实标签valid_labels之间的平均绝对误差(MAE),并将其存储在MAE_scores字典中,以便后续查看各项的MAE值。
  9. 将测试集的预测结果test_pred存储在最终的提交数据submit中,对应的列名是当前预测的标签pred_label
  10. 完成所有待预测标签的循环后,将最终的提交数据submit保存到submit_result.csv文件中。
# 从所有待预测特征中依次取出标签进行训练与预测。
for pred_label in tqdm(pred_labels):
    # print("当前的pred_label是:", pred_label)
    train_features = time_feature(train_set, pred_labels=pred_labels) # 处理训练集的时间特征。
    # train_features = enhancement(train_features_raw)
    train_labels = train_set[pred_label] # 训练集的标签数据。
    # print("当前的train_labels是:", train_labels)
    train_data = lgb.Dataset(train_features, label=train_labels) # 将训练集转换为 LightGBM 可处理的类型。

    valid_features = time_feature(valid_set, pred_labels=pred_labels) # 处理验证集的时间特征。
    # valid_features = enhancement(valid_features_raw)
    valid_labels = valid_set[pred_label] # 验证集的标签数据。
    # print("当前的valid_labels是:", valid_labels)
    valid_data = lgb.Dataset(valid_features, label=valid_labels) # 将验证集转换为 LightGBM 可处理的类型。

    # 训练模型,参数依次为:导入模型设定参数、导入训练集、设定模型迭代次数(200)、导入验证集、禁止输出日志
    model = lgb.train(lgb_params, train_data, 200, valid_sets=valid_data, callbacks=[no_info])

    valid_pred = model.predict(valid_features, num_iteration=model.best_iteration) # 选择效果最好的模型进行验证集预测。
    test_pred = model.predict(test_features, num_iteration=model.best_iteration) # 选择效果最好的模型进行测试集预测。
    MAE_score = mean_absolute_error(valid_pred, valid_labels) # 计算验证集预测数据与真实数据的 MAE。
    MAE_scores[pred_label] = MAE_score # 将对应标签的 MAE 值 存入评分项中。

    submit[pred_label] = test_pred # 将测试集预测数据存入最终提交数据中。
     
submit.to_csv('submit_result.csv', index=False) # 保存最后的预测结果到 submit_result.csv

到这里,基础版baseline的讲解就告一段落了。接下来让我们看看进阶版的baseline,针对特征工程进行了优化,在分数表现上也取得了较好的结果。

进阶版baseline

在特征工程方面,初步优化的baseline提取了更多特征。主要构建了交叉特征、历史平移特征、差分特征、和窗口统计特征。

这是大佬给出来的说明:

(1)交叉特征:主要提取流量、上部温度设定、下部温度设定之间的关系;

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

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

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

代码如下:

# 交叉特征
for i in range(1,18):
    train[f'流量{i}/上部温度设定{i}'] = train[f'流量{i}'] / train[f'上部温度设定{i}']
    test[f'流量{i}/上部温度设定{i}'] = test[f'流量{i}'] / test[f'上部温度设定{i}']
    
    train[f'流量{i}/下部温度设定{i}'] = train[f'流量{i}'] / train[f'下部温度设定{i}']
    test[f'流量{i}/下部温度设定{i}'] = test[f'流量{i}'] / test[f'下部温度设定{i}']
    
    train[f'上部温度设定{i}/下部温度设定{i}'] = train[f'上部温度设定{i}'] / train[f'下部温度设定{i}']
    test[f'上部温度设定{i}/下部温度设定{i}'] = test[f'上部温度设定{i}'] / test[f'下部温度设定{i}']
    
# 历史平移
for i in range(1,18):
    train[f'last1_流量{i}'] = train[f'流量{i}'].shift(1)
    train[f'last1_上部温度设定{i}'] = train[f'上部温度设定{i}'].shift(1)
    train[f'last1_下部温度设定{i}'] = train[f'下部温度设定{i}'].shift(1)
    
    test[f'last1_流量{i}'] = test[f'流量{i}'].shift(1)
    test[f'last1_上部温度设定{i}'] = test[f'上部温度设定{i}'].shift(1)
    test[f'last1_下部温度设定{i}'] = test[f'下部温度设定{i}'].shift(1)

# 差分特征
for i in range(1,18):
    train[f'last1_diff_流量{i}'] = train[f'流量{i}'].diff(1)
    train[f'last1_diff_上部温度设定{i}'] = train[f'上部温度设定{i}'].diff(1)
    train[f'last1_diff_下部温度设定{i}'] = train[f'下部温度设定{i}'].diff(1)
    
    test[f'last1_diff_流量{i}'] = test[f'流量{i}'].diff(1)
    test[f'last1_diff_上部温度设定{i}'] = test[f'上部温度设定{i}'].diff(1)
    test[f'last1_diff_下部温度设定{i}'] = test[f'下部温度设定{i}'].diff(1)
    
# 窗口统计
for i in range(1,18):
    train[f'win3_mean_流量{i}'] = (train[f'流量{i}'].shift(1) + train[f'流量{i}'].shift(2) + train[f'流量{i}'].shift(3)) / 3
    train[f'win3_mean_上部温度设定{i}'] = (train[f'上部温度设定{i}'].shift(1) + train[f'上部温度设定{i}'].shift(2) + train[f'上部温度设定{i}'].shift(3)) / 3
    train[f'win3_mean_下部温度设定{i}'] = (train[f'下部温度设定{i}'].shift(1) + train[f'下部温度设定{i}'].shift(2) + train[f'下部温度设定{i}'].shift(3)) / 3
    
    test[f'win3_mean_流量{i}'] = (test[f'流量{i}'].shift(1) + test[f'流量{i}'].shift(2) + test[f'流量{i}'].shift(3)) / 3
    test[f'win3_mean_上部温度设定{i}'] = (test[f'上部温度设定{i}'].shift(1) + test[f'上部温度设定{i}'].shift(2) + test[f'上部温度设定{i}'].shift(3)) / 3
    test[f'win3_mean_下部温度设定{i}'] = (test[f'下部温度设定{i}'].shift(1) + test[f'下部温度设定{i}'].shift(2) + test[f'下部温度设定{i}'].shift(3)) / 3

直接跑新的baseline已经能取得很不错的结果了,使用飞桨平台cpu核心数高的环境来跑,一般都能到6.4左右。特征工程起到了很关键的作用,大幅提高了结果分数,但是我们还是有许多可以进一步优化的地方。

主要的优化方向可能在数据预处理和防止过拟合上。可以看到,我们的baseline和初步优化后的baseline都是没有探索性数据分析(EDA)的,我们不知道数据集中有多少异常值、空值等,进行初步数据分析与可视化后,我们再选用合适的方式对数据进行处理,这样应该也能提升一点分数。

baseline代码中也没有对损失进行可视化,通过查看日志,我们了解到模型存在过拟合现象,可以使用交叉验证的方式来增强模型的泛化能力,也可以使用lightgbm自带的early_stopping_rounds参数。