☰
电力AI大赛全流程实战:从数据处理到模型部署避坑指南
2026/10/6 10:22:47 网站建设 项目流程

简介:这份压缩包是大航杯“智造扬中”电力AI大赛参赛作品的源码工程,面向备战电赛、关注电力智能化方向的本硕学生及算法爱好者,可用于快速了解电力负荷预测类赛题的完整实现链路。包内共18个文件,以6个Python脚本为核心,覆盖数据划分、特征提取、模型构建、结果可视化和预测输出等关键环节;另含4个csv原始数据表、2个xml工程配置、1个sh一键运行脚本、1张特征重要性图、readme说明及运行日志,整体约835KB,结构紧凑,便于本地复现。数据表对应典型电力预测赛题数据,适合练习时间序列特征构造与模型调参。从文件组织看,工程还附有PyCharm工程文件(.idea、.iml),打开后即可直接运行,省去环境配置的麻烦。目前已有44人学习浏览,虽然是轻量源码包,但完整呈现了从数据预处理到模型评估的竞赛思路;适合想借鉴赛事代码风格、梳理电力AI项目流程的读者,也可作为毕业设计或课程项目的落地参考。

1. 电赛“大航杯”智造扬中电力AI大赛:题目还没读透,别急着解压 zip

每年电赛季,群里都会出现类似“大航杯”这类行业冠名赛事的压缩包。这个“智造扬中”电力AI大赛,本质是主办方把真实电力场景的数据和任务打包成_1.zip,让参赛者在有限时间内用 AI 模型解决负荷预测、设备缺陷识别、异常用电检测这类落地问题。它和纯算法竞赛不一样,更多考的是你面对不干净的真实数据,怎么快速搭建一条可靠的 AI 流水线,并且能说清楚每个参数为什么这么设。适合电子、自动化、计算机方向的参赛学生,也适合现场工程师把它当成一次真实业务演练。拿到压缩包第一件事不是解压跑模型,而是把赛题描述、数据口径和提交格式弄清楚,否则非常容易白忙一场。

2. 拆解电力AI赛题:先定任务类型,再把 zip 变成可复现的项目战场

2.1 电力AI大赛最常见的三类任务:预测、识别与异常检测

很多参赛者一上来就问“用什么模型”,但模型选型完全取决于任务类型。从近几年的电力行业赛来看,绝大多数赛题都逃不开这三类:

任务类型典型赛题场景常用模型
时间序列预测区域负荷预测、光伏功率预测、电价预测LSTM、GRU、LightGBM、Prophet
视觉识别表计读数、绝缘子缺陷检测、塔基异物识别YOLO、Faster R-CNN、ResNet
异常检测窃电识别、设备故障预警、负荷异常波动IsolationForest、自编码器、时序Transformer

先识别任务类型的方法是打开压缩包看文件后缀:如果里面是一张带timestamp和load的 CSV,那大概率是时间序列预测;如果是一堆巡检图片加 label 文件,那就是视觉目标检测;如果是一个大表带用户ID和用电量标签,往往要做异常检测。拿到_1.zip后,我一般会把所有文件名列出来,随手做个草稿版任务拆解图:数据是什么、预测目标是什么、评估指标是什么、提交格式是什么,四个问题先答全,再开始写代码。

这里有个很常见的误用:有人明明做负荷预测,看到有图像数据就直接上了 YOLO,结果发现那些图片只是辅助背景资料,根本不是赛题主体。这种翻车往往是因为没有先把训练集测试集字段看完。所以我不建议在解压之前就到处找“某个模型能不能直接用”。与其纠结用什么结构,不如先把数据看成什么形态定下来。

2.2 解压_1.zip后的标准动作:校验目录、读 README、固定 Python 环境

拿到压缩包,第一步是查看而不是解压。我习惯用unzip -l先看内容清单,避免文件目录太乱或者压缩包本身损坏:

unzip -l 大航杯“智造扬中”电力AI大赛_1.zip mkdir -p electric_ai unzip -o 大航杯“智造扬中”电力AI大赛_1.zip -d electric_ai cd electric_ai ls -laR | head -40

这里的-l只列出压缩包内部文件清单,不解压,方便快速确认有没有README、data、sample_submission这类目录。-o表示覆盖已有文件,-d指定解压目录,避免把文件散落在当前目录。电力赛事数据包经常带中文目录名,Linux 下如果出现乱码,可以先执行export LC_ALL=zh_CN.UTF-8再看;Windows 的话我用 7-Zip 打开基本不会乱码。

解压后先读 README 或赛题说明,这里往往写着数据字段解释、评估指标和提交样例。然后立即创建虚拟环境,不要直接用系统 Python:

python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install --upgrade pip pip install pandas numpy matplotlib scikit-learn

如果包里有requirements.txt,直接pip install -r requirements.txt会更省事。没有的话,先装最少依赖把数据看明白,再按后续建模需求补 torch、ultralytics 这些。为什么不用系统环境?因为电赛周期短,经常要在不同电脑之间迁移环境,虚拟环境里的版本锁定能避免“队友那边复现不了”的尴尬。

2.3 数据探索脚本:先看分布、缺失与时序,再决定要不要造特征

数据探索是整个赛题里性价比最高的一步。我先花 20 分钟跑一遍下面的脚本,确认数据长什么样:

import pandas as pd import numpy as np df = pd.read_csv("data/train.csv", parse_dates=["timestamp"]) print("shape:", df.shape) print("columns:", df.columns.tolist()) print("dtypes:\n", df.dtypes) print("missing ratio:\n", df.isna().mean()) print("describe:\n", df.describe(include="all")) # 按小时聚合,看日周期性 df["hour"] = df["timestamp"].dt.hour print(df.groupby("hour")["load"].agg(["mean", "std", "median"]))

parse_dates用来把时间列转成datetime类型,只有这样后面才能用.dt.hour提取小时。isna().mean()输出每一列的缺失比例,如果缺口超过 10%,后续就要考虑插值还是剔除;descriibe能看出负荷量级,是几千瓦还是几兆瓦,直接关系归一化参数的设置。按小时聚合是判断周期性的第一步:电力负荷一般会有早晚两个峰,如果mean在夜里明显低、中午和傍晚高,说明这个数据集具备日周期性,滑窗模型天然适合。

接下来我会再画一下原始序列和箱线图:

import matplotlib.pyplot as plt fig, ax = plt.subplots(2, 1, figsize=(10, 6)) ax[0].plot(df["timestamp"], df["load"], alpha=0.3) ax[0].set_title("load time series") ax[1].boxplot(df["load"], vert=False) ax[1].set_title("load distribution") plt.tight_layout() plt.show()

这一步能暴露很多坑:如果有大量飞刺,可能是采集传感器故障,也可能是真实用电冲击,需要在训练阶段单独处理;如果分布右侧尾巴特别长,直接做 MSE 回归容易被少数大值带偏,可以考虑先取对数。我在真实项目里遇到过数据里混了一天“零负荷”,原因是停电检修,这种数据如果不清理,模型学到的就不是正常用电行为。探索完之后,我会记录每列的含义、缺失情况和周期图形,这一步对后面做特征工程和答辩都很有用。

3. 电力负荷预测的基线方案:LSTM与Transformer的选型和关键参数

3.1 为什么先用 LSTM 打底:序列模型在电力负荷上的适用边界

如果赛题确认是时间序列预测,我最先跑通的往往是一个 LSTM 基线,而不是直接上 Transformer。电力负荷有很强的日周期、周周期和季节性,LSTM 的门控机制能够把最近几个采样点的趋势记忆下来,同时对中长期周期也有一定的捕捉能力。更重要的是,电赛时间有限,数据量通常不超过几十万行,LSTM 在 CPU 上也能在几十分钟内完成一个 baseline 的训练,方便快速验证数据处理流程有没有错。

但 LSTM 不是万能的。如果数据记录跨度短、特征只有单变量,或者样本量少于几千条,LSTM 很容易过拟合,这时用 LightGBM 加滞后特征反而更稳。如果时间序列特别长且需要预测多步,LSTM 会出现误差累积,后续才考虑 Transformer 或者 Seq2Seq 结构。所以我的顺序永远是:先做数据探索,确认周期性,再决定要不要把 LSTM 当主力,而不是一上来就搭大模型。

3.2 从数据集到训练集:滑窗长度、归一化与数据集划分的工程参数

确定建模方向后,第一步是把 CSV 转成监督学习格式。电力负荷预测一般采用“滑窗”方式:用过去 N 个时刻的特征预测未来 M 个时刻的负荷。我常用的序列构造函数如下:

def make_dataset(data, feature_cols, target_col, window=48, horizon=24): X, y = [], [] features = data[feature_cols].values targets = data[target_col].values for i in range(len(data) - window - horizon + 1): X.append(features[i:i + window]) y.append(targets[i + window:i + window + horizon]) return np.array(X), np.array(y)

window=48的含义是:如果数据按半小时一个点,48 个点就是一天的负荷曲线;如果数据按小时一个点,48 个点就是两天。horizon=24表示预测未来 24 个采样点,也就是一天。这两个参数不是拍脑袋,而是要看主办方要求预测的长度。如果测试集要求预测未来 24 小时,而数据粒度是 15 分钟,那么horizon=96,window最好覆盖至少一个完整周期,即 96 或 192。

归一化是另一个必须小心的动作。正确做法是只在训练集上fit,再把验证集和测试集transform:

from sklearn.preprocessing import StandardScaler train_len = int(len(df) * 0.8) train_data = df.iloc[:train_len] test_data = df.iloc[train_len:] scaler = StandardScaler() scaler.fit(train_data[feature_cols]) train_scaled = scaler.transform(train_data[feature_cols]) test_scaled = scaler.transform(test_data[feature_cols])

这里fit只能接触训练数据的均值和方差,如果先全量fit再去切分,测试集的统计信息就偷偷混进了训练过程,这就是标准的数据泄漏。另一个细节是时间序列的划分必须按时间顺序,不能随机采样;我一般会把最后 20% 的数据当作验证集,模拟“用过去预测未来”的真实场景。

3.3 Transformer 预测负荷的两个关键点:位置编码和训练稳定性

想从 LSTM 提分到 Transformer,需要先理解两个工程关键点。第一是位置编码,Transformer 的注意力机制本身不分时间先后,必须把时刻顺序信息显式加进去。我常用的正弦位置编码如下:

import torch import numpy as np import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=512): super().__init__() pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-np.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) self.register_buffer("pe", pe) def forward(self, x): return x + self.pe[:x.size(1)]

div_term使用10000作为基数,是原始 Transformer 论文里的做法,让不同频率的正弦波编码不同时间尺度。register_buffer让位置编码跟模型一起移动到 GPU,但不会参与训练。对电力负荷这种强周期性数据,相对位置编码往往比绝对位置编码更可靠,因为它更关注“昨天这时候”和“前天这时候”的差异,而不是具体某个绝对时间。

第二是训练稳定性。Transformer 比 LSTM 更容易出现 loss 震荡,尤其在数据量不足时。我一般会给模型加一个 warmup 学习率策略:前 5 个 epoch 把学习率从很小逐渐升到目标值,后面再用余弦衰减;同时开启梯度裁剪,max_grad_norm设为 1.0。这个小习惯能避免训练到第 20 个 epoch 时突然发散。还有一点,电力负荷的大尖峰会主导训练 loss,可以在 loss 函数里对高负荷时段降低权重,或者对目标列做 log1p 变换,这些都比盲目加模型层数有效。

4. 电力视觉任务落地:从标注到部署的完整闭环

4.1 用 YOLO 做设备缺陷检测:标注格式转换和最小训练脚本

如果压缩包里是巡检图片,那大概率要做设备缺陷检测。视觉赛题里最常见的问题是标注格式不统一,很多主办方给的标注是多边形 JSON,而 YOLO 训练需要的是每行一个类别 cx cy w h的 txt。我一般会写一个转换函数:

import json def labelme2yolo(json_path, out_txt_path, class_names, img_w, img_h): with open(json_path, encoding="utf-8") as f: data = json.load(f) with open(out_txt_path, "w", encoding="utf-8") as out: for shape in data["shapes"]: cls_name = shape["label"] if cls_name not in class_names: continue points = shape["points"] x_min = min(p[0] for p in points) y_min = min(p[1] for p in points) x_max = max(p[0] for p in points) y_max = max(p[1] for p in points) cx = ((x_min + x_max) / 2) / img_w cy = ((y_min + y_max) / 2) / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h out.write(f"{class_names.index(cls_name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}\n")

这个脚本把标注中的多边形缩成外接矩形,转换时要求传入原始图片宽高,因为 JSON 里的坐标是像素坐标,必须除以宽高才能变成 YOLO 需要的归一化坐标。转换完所有标注后,用下面的最小脚本训练:

from ultralytics import YOLO model = YOLO("yolov8n.pt") # 优先选轻量模型,快速验证 model.train( data="dataset/data.yaml", epochs=50, imgsz=640, batch=8 )

data.yaml是关键,里面至少包含:train和val路径、nc类别数、names类别名。imgsz=640是默认输入尺寸,对大多数电力设备检测够先跑通,但后面要针对小目标专门调。我用小模型yolov8n当第一次校验,因为训练快,能立刻发现数据路径、标注格式和类别数的问题。

4.2 小目标与遮挡的四个处理手段:切图、拼图、锚框和多尺度

电力设备检测最大的痛点是缺陷小、遮挡多,像绝缘子破损可能只占整张巡检图的几个像素。直接拿原始图训,模型容易漏检。我常用的手段是切图训练:

import cv2 from pathlib import Path def slice_image(img_path, save_dir, size=640, overlap=0.25): img = cv2.imread(str(img_path)) h, w = img.shape[:2] step = int(size * (1 - overlap)) stem = Path(img_path).stem for y in range(0, h - size + 1, step): for x in range(0, w - size + 1, step): patch = img[y:y + size, x:x + size] out_path = Path(save_dir) / f"{stem}_{x}_{y}.jpg" cv2.imwrite(str(out_path), patch)

切图能放大目标在输入图像中的占比,但要注意同步平移标注框。overlap=0.25表示相邻切块有 25% 的重叠,避免目标恰好被切在边缘而丢失。第二个手段是拼图,把多张缩小的巡检图拼成一张大图,在 batch 维度上提高利用率。第三个是锚框,YOLO 会自动聚类锚框,但在电力小目标场景下可以显式增大输入分辨率,比如把imgsz从 640 调到 960,我经常靠这一步把 recall 拉高 3-5 个点。第四个是多尺度训练,开启 YOLO 自带的mosaic和multi-scale参数,虽然训练会慢一些,但泛化能力明显更好。

需要注意,切图后如果目标被切断,要么舍弃被切断的小目标,要么在训练时加入 overlap 让模型看到更完整的上下文。我在实际项目里发现,size=480配overlap=0.5对小目标更友好,但训练时间几乎翻倍,所以这个参数需要根据显存和比赛周期去权衡。

4.3 模型导出与部署:ONNX 与 TensorRT 的坑

比赛评审如果要求现场演示或部署,光有训练好的 PyTorch 权重是不够的。我通常会导出一版 ONNX 模型,再用 TensorRT 做推理加速:

yolo export model=best.pt format=onnx dynamic=True onnxsim best.onnx best_sim.onnx trtexec --onnx=best_sim.onnx --saveEngine=best.engine --minShapes=images:1x3x640x640 --optShapes=images:8x3x640x640 --maxShapes=images:16x3x640x640

第一条命令把 PyTorch 模型转成 ONNX;dynamic=True让模型支持动态 batch,否则部署时只能吃固定尺寸。第二条用onnxsim做图优化,去掉冗余算子。第三条通过trtexec生成 TensorRT 引擎,指定最小、最优和最大输入形状,--fp16可以进一步提速,但如果部署机器不支持半精度,会出现精度下降甚至推理失败。

导出后一定要做一次推理验证,而不是直接拿来部署:

import onnxruntime as ort import numpy as np session = ort.InferenceSession("best_sim.onnx") name = session.get_inputs()[0].name fake_input = np.random.randn(1, 3, 640, 640).astype(np.float32) outputs = session.run(None, {name: fake_input}) print(len(outputs[0]))

这个脚本能确认 ONNX 模型的输入输出是否正常。我在一次实际演示中遇到过这个问题:本地 PyTorch 推理一切正常,但 ONNX 导出后检测结果全为空,排查半天发现是输入 tensor 的通道顺序由CHW变成了HWC。所以任何模型转换,都要先拿一张真实图片对齐输出,再谈性能指标。

5. 电力AI大赛最容易翻车的5个坑:数据泄漏、指标虚高与后处理缺失

5.1 数据泄漏:用全量数据做归一化,训练 loss 低但线上崩

现象:训练集和验证集指标都非常漂亮,但第一次提交线上分数远低于本地。

原因:最常见的是归一化时用了全量数据的均值和方差,把测试集的统计信息提前暴露给了模型。另一个来源是特征构造时不小心用了未来信息,比如用目标时刻当天的平均气温作为特征,但这个值在预测时拿不到。

解决:所有预处理都必须在训练集上fit,验证集和测试集只能transform。特征工程阶段,在代码注释里标注每个特征在预测时是否可见,不可见的特征一律不进入模型。

5.2 时间序列乱切分:随机 split 导致未来信息泄漏

现象:用train_test_split(test_size=0.2, random_state=42)切分后,模型验证集得分远高于真实预测效果。

原因:随机打乱后,模型看到的验证集样本可能是训练集“未来”的邻居,时间序列的连续性和依赖关系被破坏。

解决:一律按时间顺序切分,训练集用前 80%,验证集用最后 20%。还要注意,批量构造样本时相邻样本高度重合,如果验证集采样点距离训练集过近,依然会有轻微泄漏,建议在验证集前空出一段 gap,比如留出 48 个采样点不训练也不评估。

5.3 评估指标单一:只看 RMSE 被个别尖峰带偏

现象:RMSE 很低,但画出来预测曲线整体比真实值滞后一天,或者某些天误差特别大。

原因:RMSE 对极端值敏感,个别尖峰会拉高 loss,模型为了压低 RMSE 会过度拟合这些尖峰,反而牺牲了整体形态。

解决:同时看 MAE、MAPE 和 P95 误差。MAPE 能反映相对误差,P95 能暴露尾部风险。我最常用的是把验证集的预测值和真实值按周切片画出对比曲线,如果曲线有明显的“错位”,说明模型只是记住了上一时刻的真实值,而不是真在预测未来。

5.4 提交格式不一致:预测条数、列顺序与示例不同

现象:本地跑分正常,但提交后系统反馈“格式错误”或直接零分。

原因:很多人输出预测时用 DataFrame 的默认索引,而主办方要求按id或timestamp排序;还有人是多列预测,列名和sample_submission不完全一致。

解决:提交前用print(submission.head())和print(sample.head())做肉眼对比,再用assert submission.shape == sample.shape检查尺寸。更稳妥的是把样本文件里的空值所在位置当作“要预测的格子”,按同样的行顺序写入。

5.5 环境不可复现:换台机器结果对不上

现象:代码在队友电脑上跑出来的分数和自己差很多,明明用的是同一个分支。

原因:依赖包版本不一致,PyTorch 或 scikit-learn 的随机算法实现有变动;另一个原因是每台机器的 CPU 线程数不同,很多库默认会改变随机行为。

解决:把requirements.txt锁定到具体版本,而不是>=;在训练脚本全局设置随机种子,包括random、numpy和torch。这个坑看似小事,但赛后复盘时最折磨人,我在第一次参赛时为此 debug 了两天。

6. 冲刺高分:用后处理与集成把排名再拉一截

6.1 预测结果后处理:用物理约束过滤异常尖刺

视觉模型和时序模型在后处理策略上差别很大,但目的都是在“模型输出”到“最终提交”之间加一道安全网。对于负荷预测,我会先对模型输出的多步预测做一次三维中值滤波,消除单点突变:

import numpy as np from scipy.ndimage import median_filter # y_pred shape: (n_samples, horizon) filtered = median_filter(y_pred, size=(1, 3), mode="reflect") # 根据历史数据设定物理上下界 filtered = np.clip(filtered, load_min, load_max)

median_filter的size=(1, 3)表示只在时间维上取三点中值,不跨样本混叠;load_min和load_max从训练集中取,比如历史上负荷没有低于 0,也没有高于某个容量上限,预测结果就不应该超出这个区间。这个处理能把肉眼可见的毛刺消掉,但要小心不要把真实的负荷高峰也削平。我一般会在验证集上做一次后处理对比,确认它对 RMSE 和 MAE 都没有坏影响,再决定是否用于提交。

6.2 模型集成与伪标签:稳定性的代价

比单模型更进一步的是模型集成。常见做法是把 LSTM 和 LightGBM 的预测结果在逆归一化之后做加权平均:

pred = 0.6 * pred_lstm + 0.4 * pred_gbdt

权重不是拍脑袋来的,而是在验证集上用一个简单线性回归拟合两个预测和真实值的最佳比例。集成能明显降低方差,但也可能掩盖某个模型的特殊优势,所以我只会在两个模型误差分布高度互补时才用,而不是无脑平均。伪标签在电赛里也可以用:把测试集里高置信度的预测结果当作训练样本继续微调模型,但必须设置置信度阈值并配合交叉验证,避免模型把自身的错误判断学进去。

我自己的习惯是,每次提交前都用最后三周的数据做一次滚动回测,把预测误差按星期几分组统计。如果周一和周末的误差差异很大,就回去检查特征里是否缺少“节假日”标记。真实电力场景里,模型结构只占最后一点分数,真正拉开差距的是数据处理和异常处理。希望这个思路能帮你少走弯路,也希望你在赛场上拿到让自己满意的名次。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询