MLP及其在预测中的应用
一、 核心基础:多层感知机 (MLP) 的原理
要理解 MLP,我们可以把它想象成一个“智能信息加工厂”。它的核心任务是:接收一堆杂乱的数据,通过层层提炼,找出现象背后的复杂规律。
从“直来直去”到“层层抽象”
单层感知机(太笨):只有输入层和输出层。它就像一个不会变通的基层员工,只能处理最简单的“非黑即白”的线性问题。
多层感知机(聪明):在输入和输出之间,加入了隐藏层(中间商/分析团队)。数据每经过一层隐藏层,就会被重新组合、扭曲、提取出更高级的特征。
MLP 的两大“魔法武器”
权重与偏置:神经元之间的连线粗细不同,代表了不同输入数据的重要性。模型通过不断试错,自动把这些“连线权重”调到最佳比例。
非线性激活函数(如 ReLU):这是 MLP 的灵魂。如果没有它,MLP 哪怕有一万层,最终也只是一条死板的直线。激活函数赋予了模型“扭曲空间”的能力,让它能够完美贴合极其复杂的现实曲线(这就是数学上的万能逼近定理)。
二、 巧妙转换:MLP 在时序(电力负荷)预测中的应用
MLP 天生有一个致命弱点:它没有“记忆”,不懂“时间流逝”。它只能看懂静态的表格数据。那么,怎么用它来预测未来呢?
破局之法:滑动窗口 (Sliding Window)
我们不能直接把一条连贯的时间线塞给 MLP。我们需要用一个“固定大小的框”(比如过去 24 小时),在时间线上一步步往右滑动。
框里的历史数据(昨天、前天),被强行切断,变成了表格里的静态特征(特征1、特征2...);框右边的下一个数据,就是我们要预测的目标标签。
把“时间规律”变成“空间权重”
经过重塑后,MLP 并不关心“时间是几点”。它只知道:表格里的“特征 1”和“特征 24”似乎对结果影响很大。
于是,它给这两个位置分配了极高的权重。就这样,原本随着时间波动的周期性规律(比如每天早晚的高峰期),就被 MLP 巧妙地转化为了静态的数学权重公式,从而实现了对未来的推导。
三、 严谨考试:交叉验证与隐藏层数量的选择
既然 MLP 的隐藏层这么厉害,是不是层数越多、神经元越多越好?绝对不是!隐藏层太多,模型就会变成一个“死记硬背的书呆子”(过拟合)。它把历史数据里的噪音和偶然误差全都背了下来,一遇到没见过的新数据(未来),预测直接崩溃。
为了找到最合适的隐藏层数量(超参数),我们需要一套公平的“考试制度”——交叉验证。
交叉验证的本质
就是不把所有数据都拿去训练,而是故意留出一部分作为“模拟卷(验证集)”。模型学完后,立刻用模拟卷考一考,计算出真实的预测误差。多次切分、多次考试求平均分,就能选出表现最稳健的模型结构。
时序预测的致命误区:千万别用普通 K 折!
普通 K 折交叉验证:会把数据随机打乱。这在时序中会引发灾难性的“数据泄露”——模型可能偷偷看到了周五的数据,然后去“预测”周三的结果,这在现实中是作弊。
时序交叉验证(正确姿势):必须严格遵守“时间箭头只能向前”的铁律。
考卷 1:用 1-3 月数据训练,考 4 月。
考卷 2:用 1-4 月数据训练,考 5 月。
验证集永远紧跟在训练集之后,绝不越界。只有在这种极其严苛的顺延考试中拿高分的 MLP,才是真正能在未来电力负荷预测中扛打的好模型。
通过这三步,建立起了一个清晰的 MLP 预测框架:重塑数据 -> 设定隐藏层 -> 时序交叉验证选优 -> 预测未来。
import torch import torch.nn as nn import numpy as np from sklearn.model_selection import TimeSeriesSplit # ========================================== # 0. 准备基础部件 (数据与模型) # ========================================== # 模拟生成 100 天的电力负荷数据 (已假设做了归一化) np.random.seed(42) raw_data = np.sin(np.linspace(0, 10, 100)) + np.random.normal(0, 0.1, 100) # 滑动窗口函数 (把时间转为表格) def create_windows(data, window_size=3): X, y = [], [] for i in range(len(data) - window_size): X.append(data[i : i+window_size]) y.append(data[i+window_size]) return torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.float32).unsqueeze(1) # 重塑数据:用过去 3 天预测第 4 天 X, y = create_windows(raw_data, window_size=3) # 定义我们的主角:一个包含两层隐藏层的 MLP class MLP(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(3, 16), nn.ReLU(), nn.Linear(16, 8), nn.ReLU(), nn.Linear(8, 1) ) def forward(self, x): return self.net(x) # ========================================== # 核心区域:时序交叉验证 (Time Series CV) # ========================================== # 引入时序考官:设定切分为 3 折 (考 3 场试) # 它会自动按照时间顺序,帮我们划分好训练集和验证集的索引 tscv = TimeSeriesSplit(n_splits=3) # 用来记录每次考试成绩的记分册 exam_scores = [] # 开始循环考试! # tscv.split(X) 每次会吐出一组不打乱的 [训练集索引] 和 [验证集索引] for fold, (train_index, test_index) in enumerate(tscv.split(X)): print(f"\n--- 正在进行第 {fold + 1} 场考试 ---") # 1. 严格按照时间顺序划分考题 X_train, X_test = X[train_index], X[test_index] y_train, y_test = y[train_index], y[test_index] print(f"训练集数据量: {len(X_train)} 条 | 验证集(考试)数据量: {len(X_test)} 条") # ⚠️ 致命易错点 1:每次考试,必须实例化一个全新的模型! # 如果你把 model 定义在循环外面,模型就会带着上一场的记忆继续学,这就失去了评估模型结构的意义。 model = MLP() # 定义批改标准 (均方误差) 和优化器 criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.05) # 2. 闭关学习阶段 (只看训练集) model.train() for epoch in range(50): # 让模型对历史数据学 50 遍 optimizer.zero_grad() predictions = model(X_train) loss = criterion(predictions, y_train) loss.backward() optimizer.step() # 3. 考试阶段 (绝对不准再更新权重) # ⚠️ 致命易错点 2:考试前必须开启 eval() 模式 model.eval() with torch.no_grad(): # 告诉 PyTorch,别算梯度了,我们不学习了,只做题 test_preds = model(X_test) # 计算在未见过的考试卷上的误差 test_loss = criterion(test_preds, y_test) exam_score = test_loss.item() exam_scores.append(exam_score) print(f"第 {fold + 1} 场考试误差 (MSE): {exam_score:.4f}") # ========================================== # 计算最终评价指标 # ========================================== average_score = np.mean(exam_scores) print(f"\n======================================") print(f"该 MLP 模型结构的最终综合评分 (平均误差): {average_score:.4f}") print(f"======================================")在实际操作中,把电力负荷数据喂给模型之前,通常还需要进行一个极度关键的操作:数据归一化(或者叫特征缩放)
如果你直接把原始的电力数据(比如负荷是 5000 兆瓦,温度是 25 度)喂给它,它不仅会“消化不良”,甚至可能彻底“罢工”。
这背后的数学原理和机制,主要由以下三大原因导致:
1. 核心致命伤:激活函数的“饱和”与梯度消失
我们在前面提到过,MLP 的灵魂在于隐藏层的非线性激活函数(如 Sigmoid、Tanh)。
以经典的 Sigmoid 函数为例,它的公式是 $\sigma(x) = \frac{1}{1 + e^{-x}}$。这个函数的作用是把任何输入的数字,压缩到 $0$ 到 $1$ 之间。
如果你输入 $0$,输出是 $0.5$(处于曲线中间,最敏感的区域)。
如果你输入 $20$(比如温度),输出大约是 $0.999999999$。
如果你输入 $5000$(未缩放的负荷),输出也是无限接近于 $1$。
灾难就在这里发生了:
神经网络是通过反向传播(算梯度/求导数)来学习的。当输入值非常大(如 5000)时,它落在了 Sigmoid 曲线极其平坦的边缘区域。在平坦区域,曲线的坡度(导数/梯度)几乎等于 $0$。
在数学上,梯度 = $0$ 意味着“没有误差信号可以传回来”。权重无法得到任何更新指示,神经元直接进入了“麻木/瘫痪”状态。这种现象在学术界被称为梯度消失 (Vanishing Gradient)。模型看起来在训练,实际上内部的参数根本没变,完全没有在学习。
2. “偏科”的特征:损失函数的地形被严重扭曲
在电力负荷预测中,我们通常会输入多个特征:
特征 A:昨天同一时刻的负荷(比如 4500 MW)
特征 B:当前的室外温度(比如 20 °C)
特征 C:是否为节假日(0 或 1)
如果不仅行缩放,直接扔进模型,MLP 在计算 $W \cdot X$ 时,由于 4500 比 20 大太多了,特征 A 产生的影响会以压倒性的优势掩盖掉特征 B 和 C 的作用。
不仅如此,由于不同特征的尺度相差悬殊,这会在高维数学空间中,把模型的“损失函数地形(Loss Landscape)”扭曲成一个极度狭长的峡谷。
模型在寻找最低点(最优解)时,会在峡谷的两壁之间疯狂来回震荡(Zig-zagging),走极其低效的“Z”字形路线。这会导致模型训练极其缓慢,甚至根本找不到谷底(无法收敛)。
而如果我们把所有数据缩放到统一的尺度(比如都在 $0$ 到 $1$ 之间),损失函数的地形就会变成一个规整的圆碗形。此时,模型就能直接朝着碗底全速下降,极大地加速了收敛过程。
3. 与“权重初始化”的天然冲突
当你用 PyTorch 实例化一个 MLP 时,框架会自动为你随机生成成千上万个初始权重。为了保证网络一开始的稳定,这些初始权重通常是非常小的随机数(比如在 $-0.1$ 到 $0.1$ 之间)。
设计者的初衷是:小数乘以(缩放后的)小数,网络起步平稳。
如果你强行输入 5000 这样的大数字,即使权重只有 $0.1$,乘积也会高达 500。巨大的数值在网络层间传递,要么导致激活函数瘫痪(如上述第一点),要么导致使用 ReLU 时发生梯度爆炸 (Exploding Gradient),直接算出NaN(Not a Number),模型直接崩溃。
MLP在预测的简单代码应用:
import torch import torch.nn as nn import numpy as np # ========================================== # 步骤一:模拟构造短期负荷的多元特征数据集 # ========================================== np.random.seed(42) num_samples = 200 # 模拟 200 个历史时间点 # 1. 构造特征 A:过去 3 个小时的历史负荷 (假设已经归一化在 0~1) history_load = np.random.rand(num_samples, 3) # 2. 构造特征 B:预测时刻的预测温度 (短期预测中,温度对空调负荷影响极大) temperature = np.random.rand(num_samples, 1) * 0.8 + 0.1 # 3. 构造特征 C:时间特征(比如当前是 0~23 点中的哪小时,转换为 0~1 之间的数值) hour_of_day = np.linspace(0, 1, num_samples).reshape(-1, 1) # 核心动作:将【历史负荷】、【温度】、【时间】在横轴上拼接在一起,形成一个 5 维的特征大表格 # 拼接后的每一行:[负荷t-1, 负荷t-2, 负荷t-3, 温度t, 小时t] X_raw = np.hstack((history_load, temperature, hour_of_day)) # 4. 构造目标标签 Y:当前时刻的真实负荷 # 真实负荷由历史趋势、温度飙升共同决定,并加上一些随机扰动 Y_raw = 0.5 * history_load[:, 0:1] + 0.4 * temperature + 0.1 * np.random.rand(num_samples, 1) # 转换为 PyTorch 张量 X_tensor = torch.tensor(X_raw, dtype=torch.float32) Y_tensor = torch.tensor(Y_raw, dtype=torch.float32) # ========================================== # 步骤二:搭建适用于短期预测的 MLP 网络 # ========================================== class ShortTermLoadMLP(nn.Module): def __init__(self, input_dim): super().__init__() # 这是一个典型的三层神经网络架构 (输入5维 -> 隐藏32维 -> 隐藏16维 -> 输出1维) self.network = nn.Sequential( # 第一层:接收 5 个混合特征,映射到 32 个神经元中进行初步交叉融合 nn.Linear(input_dim, 32), nn.ReLU(), # 激活函数,负责捕捉“温度超过35度后负荷呈指数上升”这种非线性规律 # 第二层:进一步提炼特征,提取更高级的作息与天气组合逻辑 nn.Linear(32, 16), nn.ReLU(), # 输出层:将所有的抽象智慧,凝聚成一个具体的预测负荷数字 nn.Linear(16, 1) ) def forward(self, x): return self.network(x) # 实例化模型。输入维度是 5(3个历史负荷 + 1个温度 + 1个时间) model = ShortTermLoadMLP(input_dim=5) # ========================================== # 步骤三:定义“批改标准”并开始训练 # ========================================== criterion = nn.MSELoss() # 均方误差,短期预测最常用的损失函数 optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # 开始循环训练 for epoch in range(150): model.train() # 前向传播:带入 5 个特征,算出预测负荷 preds = model(X_tensor) # 计算误差 loss = criterion(preds, Y_tensor) # 反向传播纠错 optimizer.zero_grad() loss.backward() optimizer.step() if (epoch + 1) % 30 == 0: print(f"第 {epoch+1} 轮迭代 | 预测误差 (MSE Loss): {loss.item():.5f}") # ========================================== # 步骤四:未来推导 (在线预测应用) # ========================================== model.eval() # 场景:现在是晚上 23:00,我们要预测 24:00 的负荷。 # 收集最新数据:过去3小时负荷是 [0.6, 0.65, 0.7],气象台预报 24点温度为 0.2,时间权重为 0.99 latest_features = torch.tensor([[0.6, 0.65, 0.7, 0.2, 0.99]], dtype=torch.float32) with torch.no_grad(): next_hour_load = model(latest_features) print(f"\n[预测成功] 明天 0:00 的电力负荷预测值为: {next_hour_load.item():.4f}")