Day 55 序列预测任务介绍

@浙大疏锦行

今日任务:

  1. 序列预测介绍:单步预测;多步预测(2种方式)
  2. 序列数据的处理:滑动窗口
  3. 多输入多输出任务的思路
  4. 经典机器学习在序列任务上的劣势

作业:手动构造类似的数据集(如cosx数据),观察不同的机器学习模型的差异

序列预测任务

序列预测:数据存在先后关系顺序),任务是根据已有的历史数据预测未来的数据。比如,股票价格预测(根据过去30天预测第31天),文本单词预测等。

滑动窗口

在结构化数据中,处理监督学习问题时,存在的是“样本—标签”数据对。而在序列任务中,也有这样类似的数据对“x-y”,实现的方法是滑动窗口(类似卷积核的滑动)。

通过设置不同的时间步(seq_length)来完成下一个时间步的预测。比如在下面的例子中,就得到了 "[10,20,30] --> 40" 的数据对。

注:最后的[70,80,90]不能作为输入(无后续值作为目标),故生成样本数:len(data) - seq_length

data = [10,20,30,40,50,60,70,80,90]
seq_length = 3
X = [
  [10, 20, 30],  # 用前3步预测第4步 -> 40
      [20, 30, 40],  # 用2-4步预测第5步 -> 50
          [30, 40, 50],  # 用3-5步预测第6步 -> 60
              [40, 50, 60],  # 用4-6步预测第7步 -> 70
                  [50, 60, 70],  # 用5-7步预测第8步 -> 80
                      [60, 70, 80]   # 用6-8步预测第9步 -> 90
]

y = [40, 50, 60, 70, 80, 90]  # 每个X对应的下一个值

预测方式

之前的结构化数据是独立同分布的,可以完成一步到位的映射;而序列数据由于自身的时间顺序性,只能一步一步地、递归地预测。

根据每一次预测的时刻数量,可分为单步预测和多步预测。

(1)单步预测:一次只预测下一时刻,多输入单输出。比如上面例子中的 [10,20,30] --> 40。

(2)多步预测:一次预测多个时刻,多输入多输出

  • 递归式(滚动预测):先用预测第81天的,再用81天的预测数据和历史数据预测第82天的,以此类推,这种方式会造成误差的累积
  • 直接式:一次性输出未来多个时刻的值。如输入 x60-x80,输出 x81-x90,不累积误差

多输入多输出任务

  • 输入:血压、血糖、胆固醇、病史、年龄

  • 输出

    1. 是否患糖尿病 (0/1) -> 任务A:二分类

    2. 冠心病风险 (概率值) -> 任务B:回归

    3. 肥胖等级 (1-5) -> 任务C:多分类

上面的例子是一个多输入多输出(MIMO)的问题,由于传统的机器学习模型默认单输出,针对这类MIMO问题可以由MLP实现,思路如下:

  • 独立建模:直接拆分为多个多输入单输出的任务,可能丢失预测标签间的影响
  • 联合建模:通过共享表示和任务特定分支,同时、协同地预测多个相关目标变量,最终损失函数是三个任务损失函数的加权和(权重分配)

实战

(1)准备工作:设置全局随机种子,复用之前的函数

# 准备工作

import numpy as np
import random
import os
import torch
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
import warnings
warnings.filterwarnings("ignore")


# 设置随机种子确保结果可复现,全局随机函数
def set_seed(seed=42, deterministic=True):
    """
    设置全局随机种子,确保实验可重复性
    
    参数:
        seed: 随机种子值,默认为42
        deterministic: 是否启用确定性模式,默认为True
    """
    # 设置Python的随机种子
    random.seed(seed) 
    os.environ['PYTHONHASHSEED'] = str(seed) # 确保Python哈希函数的随机性一致,比如字典、集合等无序
    
    # 设置NumPy的随机种子
    np.random.seed(seed)
    
    # 设置PyTorch的随机种子
    torch.manual_seed(seed) # 设置CPU上的随机种子
    torch.cuda.manual_seed(seed) # 设置GPU上的随机种子
    torch.cuda.manual_seed_all(seed)  # 如果使用多GPU
    
    # 配置cuDNN以确保结果可重复
    if deterministic:
        torch.backends.cudnn.deterministic = True
        torch.backends.cudnn.benchmark = False


# 设置随机种子
set_seed(42)

(2)数据准备:生成合成的时间序列数据、标准化、数据划分

# 1-生成合成的时间序列数据
x = np.linspace(0,100,1000)
y = np.cos(x) + 0.1 * x + np.random.randn(1000) # 余弦波+线性趋势+噪声
# 可视化原始数据
plt.figure(figsize=(12, 6))
plt.plot(y)
plt.title('Sythesized Time Sequential Data (Cos + Linear + Noise)')
plt.xlabel('Seq_length')
plt.ylabel('Value')
plt.grid(True)
plt.show()

在这里需要注意的是顺序的选择:不能按照之前机器学习的思路,直接先划分训练集和测试集,然后滑动窗口得到数据对。这样做会导致数据不全。

因此需要先对全部的数据进行滑动窗口处理,然后再划分测试集和训练集。本质上讲,就是先转换为之前的模式(有结构化数据的数据对),然后划分数据集进行后续操作。

注:标准化时使用训练数据拟合,再转换整个数据。因为真实情况下,只知道历史数据的分布。

# 2-数据划分
train_size = int(len(y) * 0.8) # 训练集占比
train_data_raw = y[:train_size] # 得到训练集

# 2-1 标准化
# 缩放器处理特征矩阵,要求输入[样本数, 特征数]
scaler = MinMaxScaler(feature_range=(0,1))
scaler.fit(train_data_raw.reshape(-1,1)) # 学习训练集
scaled_y = scaler.transform(y.reshape(-1,1)).flatten() # 转换整个数据

# 2-2 定义生成数据对的函数(滑动窗口)
def create_sequences(data,seq_length):
    X,y = [],[] # 分别存储特征和标签
    for i in range(len(data)-seq_length):
        X.append(data[i:i+seq_length])
        y.append(data[i+seq_length])

    return np.array(X),np.array(y)

seq_length = 30
all_X,all_y = create_sequences(scaled_y,seq_length) # 得到数据对
# 2-3 划分测试集和训练集 
split_idx = train_size - seq_length

X_train = all_X[:split_idx]
y_train = all_y[:split_idx]
X_test = all_X[split_idx:]
y_test = all_y[split_idx:]

# 查看大小,验证
print("原始数据总长度:", len(y))
print("训练数据原始长度:", train_size)
print("测试数据原始长度:", len(y) - train_size)
print("-" * 30)
print("序列长度 (seq_length):", seq_length)
print("滑动窗口后样本总数:", len(all_X))
print("-" * 30)
print("训练集划分点 (split_idx):", split_idx)
print("训练集特征(X_train)形状:", X_train.shape) # (770, 30) -> (800-30, 30)
print("训练集标签(y_train)形状:", y_train.shape)   # (770,)
print("测试集特征(X_test)形状:", X_test.shape)   # (200, 30) -> (1000-30 - 770, 30)
print("测试集标签(y_test)形状:", y_test.shape)     # (200,)
print("-" * 30)

(3)调整形状

序列数据的形状:[samples,seq_length,features=1](RNN),对于传统机器学习模型的输入:[samples,features]。因此需要讲具有时间顺序的三维序列数据"扁平化"为二维特征表格,从而让传统的机器学习模型(如随机森林、SVM等)能够处理时间序列预测任务(牺牲时间性)。

# 3-调整形状,适配机器学习模型
# Scikit-learn的机器学习模型需要二维的输入: [样本数, 特征数]
# RNN需要的是三维输入: [样本数, 时间步长, 特征数]
# 我们需要将每个样本的 `seq_length` 个时间步“扁平化”成 `seq_length` 个特征。
# 原始形状: (770, 30, 1) or (770, 30) -> 目标形状: (770, 30)

X_train_rf = X_train.reshape(X_train.shape[0],-1)
X_test_rf = X_test.reshape(X_test.shape[0],-1)
# y_train,y_test形状符合要求

(4)搭建模型(RF):训练、评估

按照之前的流程,核心的三行代码:实例化、训练、预测。

注:评估是要在原始尺度上(反标准化)比较,因为不同缩放的差异不同,与原始数据的误差也更具解释性。

# 4-模型
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
rf_model = LGBMRegressor(random_state=42) # 实例化
rf_model.fit(X_train_rf,y_train) # 训练
# 得到训练集和测试集的预测结果
rf_train_pred = rf_model.predict(X_train_rf)
rf_test_pred = rf_model.predict(X_test_rf)

# 5-评估:RMSE
# 5-1 反标准化,在原始尺度上评估,scaler.inverse_transform 需要二维输入,所以先 reshape
rf_train_pred_origin = scaler.inverse_transform(rf_train_pred.reshape(-1,1))
rf_test_pred_origin = scaler.inverse_transform(rf_test_pred.reshape(-1,1))

y_train_origin = scaler.inverse_transform(y_train.reshape(-1,1))
y_test_origin = scaler.inverse_transform(y_test.reshape(-1,1))
    
# 5-2 计算RMSE
train_rmse = np.sqrt(mean_squared_error(y_train_origin,rf_train_pred_origin))
test_rmse = np.sqrt(mean_squared_error(y_test_origin,rf_test_pred_origin))
print(f"\n训练集 RMSE: {train_rmse:.4f}")
print(f"测试集 RMSE: {test_rmse:.4f}")

(5)可视化结果

绘制训练集和测试集的结果

注:原始数据和预测值存在时间偏移,需要先对齐,再绘图

# 可视化
plt.figure(figsize=(15, 7))
plt.plot(y, label='Origin', color='gray', alpha=0.5)

# 绘制训练集的预测结果
# 预测值与原始数据位置不同步(时间偏移),要对齐
train_predict_plot = np.empty_like(y) # 创建一个与原始数据y形状相同的空数组
train_predict_plot[:] = np.nan # 填充为空值,实现在有预测值的位置显示线条
train_predict_plot[seq_length : len(rf_train_pred_origin) + seq_length] = rf_train_pred_origin.flatten() #填入预测值
plt.plot(train_predict_plot, label='Train Pred (RF)', color='blue')

# 绘制测试集的预测结果
test_predict_plot = np.empty_like(y)
test_predict_plot[:] = np.nan
test_predict_plot[len(rf_train_pred_origin) + seq_length : len(y)] = rf_test_pred_origin.flatten()
plt.plot(test_predict_plot, label='Test Pred (RF)', color='red')

plt.title('Time Sequence Predictions(LGBM)')
plt.xlabel('Seq_length')
plt.ylabel('Value')
plt.legend()
plt.grid(True)
plt.show()

作业

使用数据:

x = np.linspace(0,100,1000)
y = np.cos(x) + 0.1 * x + np.random.randn(1000) # 余弦波+线性趋势+噪声

其余不变,查看各模型的输出:

Random Forest 

训练集 RMSE: 0.4269 测试集 RMSE: 1.4789

XGBoost

训练集 RMSE: 0.0110 测试集 RMSE: 1.5932

LGBM

训练集 RMSE: 0.1949 测试集 RMSE: 1.7703

从上面可以看到,在训练集上,三个模型的表现都不错,其中XGBoost的误差最小,与原始数据重合度最高。但是在测试集上,三者的误差都比较大。

失败的原因根源在于决策树的工作原理:决策树及其集成模型(如随机森林)的预测基于训练数据的局部平均,其输出范围被训练集目标值的极值所严格限制,因此不具备外推能力,无法预测超出其“经验”范围的值。

  • 模型(单棵树或森林)的最终预测值是训练样本在叶子节点内的平均值。
  • 模型的预测结果严格限定在训练集目标值(y)的最小值最大值之间。
  • 模型学到的是输入与输出在训练集范围内的静态映射关系,而非抽象的、可延伸的“趋势”概念
  • 当输入/输出超出训练范围时,模型只会给出其已知的边界极值,导致预测曲线变为一条水平线,无法进行有效外推。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值