AI新能源发电功率预测(第三届世界科学智能大赛)

本次比赛主要是要进行风电和光伏电场的功率预测,基于气象数据预测10个新能源场站的功率进行推理。

一、比赛数据说明:

1.气象数据

  • 来自三个不同的气象预报源(NWP_1, NWP_2, NWP_3),每个气象源提供8个变量,包含风速、气温、降水量、云量、辐照度等数据。
  • 每个气象数据文件以.nc格式保存,包含未来24小时的1小时间隔气象预报,文件中有5个维度:
    • time:世界时刻(UTC时间)。
    • channel:表示气象变量,共8个(例如:风速、气温等)。
    • hour:从预报起始时间到每个预报时刻的时间间隔,范围是0到23小时。
    • latlon:分别是纬度和经度,标识气象数据的空间位置。

本次要求你使用这些气象数据来预测新能源场站的功率,可以选择使用一个或多个气象源的数据,但不需要使用所有的变量。

2.场站实发功率

提供了10个新能源场站(5个风电,5个光伏)的实际功率数据,已经进行了归一化处理。数据的时间间隔为15分钟(北京时间),因此每一天有96个数据点(24小时×4个时间段)。数据中可能会存在缺失值或异常值,需要注意这些问题,可能会影响最终的精度评估。

3.气象变量说明

给出了8个气象变量的具体含义和单位,如风速(u100、v100),气温(t2m),降水量(tp),辐照度(poai和ghi)等。不同气象源的数据结构稍有不同,尤其是NWP_2多了一个msl(海平面气压)。

要求格式:

Input/
├── 1/
│   ├── NWP_1/
│   │   ├── 20240101.nc
│   │   ├── 20240102.nc
│   │   └── ...
│   ├── NWP_2/
│   ├── NWP_3/
├── 2/
├── ...
Output/
├── output1.csv
├── output2.csv
├── ...

评测指标:
统计精度时,每个场站每天单独计算一个精度值,并对其在所有评测天数内的精度值求平均,得到该场站的整体精度。最终精度为十个场站精度的平均值。

每日预测准确率(CR)

每个场站在一天内的预测功率与实际功率之间的差异,以此来计算准确率。公式中涉及到计算每个时段的误差,然后将所有时段的误差进行平均。

场站预测准确率 (Cf)

每个场站的预测准确率是对该场站所有预测日(通常为1天)的准确率的平均。

 

二、分析本次赛制baseline代码

这里不会从头进行赘述,只简介其中思想。

加载数据

from netCDF4 import Dataset
import numpy as np
import pandas as pd

nc_path = "data/初赛训练集/nwp_data_train/1/NWP_1/20240101.nc"
dataset = Dataset(nc_path, mode='r')     #使用只读格式打开文件
dataset.variables.keys() #访问dataset对象的variables属性,并返回字典的keys,即所有变量名称

加载了一个.nc格式的气象数据文件,查看其中的变量。

channel = dataset.variables["channel"][:]
data = dataset.variables["data"][:]
mean_values = np.array([np.mean(data[:, :, i, :, :][0], axis=(1, 2)) for i in range(8)]).T

提取气象数据并计算每个通道的平均值。

训练数据构建

def get_data(path_template, date):
# 加载指定日期的.nc文件
    dataset = Dataset(path_template.format(date), mode='r')
# 获取气象变量名称列表
    channel = dataset.variables["channel"][:]
# 获取气象数据数组
    data = dataset.variables["data"][:]
# 计算各通道的平均值
    mean_values = np.array([np.mean(data[:, :, i, :, :][0], axis=(1, 2)) for i in range(8)]).T
    return pd.DataFrame(mean_values, columns=channel) # 返回DataFrame

train_path_template = "data/初赛训练集/nwp_data_train/1/NWP_1/{}.nc"
data = [get_data(train_path_template, i) for i in date]
train = pd.concat(data, axis=0).reset_index(drop=True)

目标数据加载

target = pd.read_csv("data/初赛训练集/fact_data/1_normalization_train.csv")
target = target[96:]
target = target[target['时间'].str.endswith('00:00')]
去除前96行数据,筛选整点数据

假设原始数据是这样的:

时间功率(MW)
2024-01-01 00:0010.5
2024-01-01 00:1510.8
2024-01-01 00:3011.2
2024-01-01 00:4511.0
......

经过处理后:

时间功率(MW)
2024-01-01 00:0010.5
2024-01-01 01:0012.1
2024-01-01 02:0013.5
......

 

测试数据构建

test_path_template = "data/初赛测试集/nwp_data_test/1/NWP_1/{}.nc"
date_range = pd.date_range(start='2024-12-31', end='2025-02-27')
date = [date.strftime('%Y%m%d') for date in date_range]
data = [get_data(test_path_template, i) for i in date]
test = pd.concat(data, axis=0).reset_index(drop=True)

模型训练

由于本次数据量中等,可以使用LightGBM(基于梯度提升决策树,GBDT),与5 折交叉验证(KFold)

def cv_model(clf, train_x, train_y, test_x):
    # 5折交叉验证设置
    folds = 5
    kf = KFold(n_splits=folds, shuffle=True)  # 数据随机打乱
    oof = np.zeros(train_x.shape[0])          # 存储验证集预测结果
    test_predict = np.zeros(test_x.shape[0])  # 存储测试集预测结果
    cv_scores = []                            # 存储每折的评估分数
    
    # 开始交叉验证
    for i, (train_index, valid_index) in enumerate(kf.split(train_x, train_y)):
        # 划分训练集和验证集
        trn_x, trn_y = train_x.iloc[train_index], train_y[train_index]
        val_x, val_y = train_x.iloc[valid_index], train_y[valid_index]
        
        # 转换为LightGBM数据集格式
        train_matrix = clf.Dataset(trn_x, label=trn_y)
        valid_matrix = clf.Dataset(val_x, label=val_y)
        
        # LightGBM参数配置
        params = {
            'boosting_type': 'gbdt',       # 使用GBDT算法
            'objective': 'regression',     # 回归任务
            'metric': 'rmse',             # 评估指标为RMSE
            'num_leaves': 256,            # 叶子节点数
            'learning_rate': 0.1,         # 学习率
            'feature_fraction': 0.8,      # 特征采样比例
            'lambda_l2': 10,              # L2正则化
            'verbose': -1                 # 不显示训练日志
        }
        
        # 训练模型(3000轮,带早停)
        model = clf.train(params, train_matrix, 3000, valid_sets=[valid_matrix])
        
        # 预测验证集和测试集
        val_pred = model.predict(val_x, num_iteration=model.best_iteration)
        test_pred = model.predict(test_x, num_iteration=model.best_iteration)
        
        # 保存结果
        oof[valid_index] = val_pred                     # 验证集预测
        test_predict += test_pred / kf.n_splits         # 测试集预测平均
        
        # 计算评估分数(1/(1+RMSE))
        score = 1/(1+np.sqrt(mean_squared_error(val_pred, val_y)))
        cv_scores.append(score)
            
    return oof, test_predict  # 返回验证集和测试集预测结果

# 调用函数训练模型
lgb_oof, lgb_test = cv_model(lgb, train, target['功率(MW)'], test)

结果输出

output = pd.read_csv("data/output/output1.csv").reset_index(drop=True)
output["power"] = lgb_test
# 将数据重复4次
lgb_test = [item for item in lgb_test for _ in range(4)]

 

三、改进代码

加载数据的方式与baseline加载方式类似,就不赘述。这里只更改模型,将使用transformer同时加入物理约束进行优化模型

首先通过使用 PositionalEncoding 对输入数据进行位置编码,解决序列数据中位置信息丢失的问题。

class PositionalEncoding(nn.Module):
    """位置编码"""

    def __init__(self, d_model: int, max_len: int = 5000):
        super().__init__()
        position = torch.arange(max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))
        pe = torch.zeros(max_len, 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: torch.Tensor) -> torch.Tensor:
        """添加位置编码"""
        x = x + self.pe[:x.size(1)]
        return x

使用了Transformer架构的编码器部分来处理气象序列数据。这个编码器通过自注意力机制(Self-Attention)学习气象数据之间的时序关系。然后从编码器输出的特征中预测未来96个时间点的功率。

同时根据是否为光伏场站(is_solar)应用不同的约束,例如:

  • 光伏场站只能在白天的有效发电时间内有功率输出(6:00到18:00)。
  • 风电场站的功率输出不能超过最大风电功率。      

加入物理约束,具体代码如下:

class Transformer(nn.Module):
    def __init__(self, input_dim: int = 8, num_heads: int = 4, forecast_steps: int = 96):
        super().__init__()
        self.input_dim = input_dim
        self.forecast_steps = forecast_steps

        # 输入嵌入层
        self.embedding = nn.Sequential(
            nn.Linear(input_dim, 64),
            nn.GELU(),
            nn.LayerNorm(64)
        )

        # Transformer编码器
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=64, nhead=num_heads, dim_feedforward=256,
            dropout=0.1, activation='gelu', batch_first=True
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=4)

        # 解码器
        self.decoder = nn.Sequential(
            nn.Linear(64, 128),
            nn.GELU(),
            nn.Dropout(0.2),
            nn.Linear(128, forecast_steps)
        )

        # 位置编码
        self.pos_encoder = PositionalEncoding(64)

    def forward(self, x: torch.Tensor, is_solar: bool = False) -> torch.Tensor:
        """输入: (batch, lookback, input_dim)"""
        x = self.embedding(x)  # (batch, lookback, 64)
        # 位置编码
        x = self.pos_encoder(x)
        x = self.transformer(x)
        x = x.mean(dim=1)  # (batch, 64)
        # 预测
        output = self.decoder(x)
        # 物理约束
        output = self.apply_constraints(output, is_solar)
        return output
    def apply_constraints(self, outputs: torch.Tensor, is_solar: bool) -> torch.Tensor:
        """应用物理约束"""
        outputs = torch.clamp(outputs, min=0)  # 所有功率非负

        if is_solar:  # 光伏场站
            hour_idx = torch.arange(24).repeat_interleave(4)[:96].to(device)
            mask = (hour_idx < SOLAR_HOURS[0]) | (hour_idx >= SOLAR_HOURS[1])
            outputs[..., mask] = 0
        else:  # 风电场站
            outputs = torch.clamp(outputs, max=MAX_WIND_POWER)

        return outputs

训练模型过程使用自适应学习率,优化器使用AdamW

训练过程中使用 Huber损失 来度量模型的误差

def train_model(dataset: SimpleDataset, model: nn.Module, epochs: int = 20):
    """训练模型"""
    loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=0)

    optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=0.01)
    scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, epochs)
    criterion = nn.HuberLoss()

    model.train()
    for epoch in range(epochs):
        total_loss = 0
        pbar = tqdm(loader, desc=f"Epoch {epoch + 1}/{epochs}")

        for nwp_seq, power in pbar:
            nwp_seq, power = nwp_seq.to(device), power.to(device)

            optimizer.zero_grad()
            pred = model(nwp_seq, dataset.is_solar)
            loss = criterion(pred, power)
            loss.backward()
            nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()

            total_loss += loss.item()
            pbar.set_postfix(loss=loss.item())

        scheduler.step()
        print(f"Epoch {epoch + 1} Avg Loss: {total_loss / len(loader):.4f}")

预测过程中,对于异常值进行补零操作

def predict(model: nn.Module, dataset: SimpleDataset, expected_length: int = 5568) -> np.ndarray:
    """生成预测结果(修复维度不匹配问题)"""
    loader = DataLoader(dataset, batch_size=32, shuffle=False, num_workers=0)

    model.eval()
    all_preds = []
    with torch.no_grad():
        for nwp_seq, _ in tqdm(loader, desc="Predicting"):
            # 确保输入数据形状正确
            if nwp_seq.dim() == 2:
                nwp_seq = nwp_seq.unsqueeze(0)  # 添加batch维度

            pred = model(nwp_seq.to(device), dataset.is_solar)

            # 确保预测输出形状正确
            if pred.dim() == 1:
                pred = pred.unsqueeze(0)  # 添加batch维度

            all_preds.append(pred.cpu().numpy())

    # 合并所有预测结果
    if len(all_preds) > 0:
        preds = np.concatenate(all_preds, axis=0)  # 沿batch维度连接
        preds = preds.reshape(-1)  # 展平为一维数组

        # 调整到预期长度
        if len(preds) < expected_length:
            # 不足部分用最后一天的数据循环填充
            last_day = preds[-96:]
            repeat_times = (expected_length - len(preds)) // 96 + 1
            padding = np.tile(last_day, repeat_times)[:expected_length - len(preds)]
            preds = np.concatenate([preds, padding])
        elif len(preds) > expected_length:
            # 超过部分截断
            preds = preds[:expected_length]
    else:
        # 如果没有预测结果,生成全零数组
        preds = np.zeros(expected_length)

    return preds

最终将outputs放入zip文件打包

def save_predictions(preds: np.ndarray, station_id: int):
    """保存预测结果(确保格式正确)"""
    os.makedirs("output", exist_ok=True)
    assert len(preds) == 5568, f"预测结果长度应为5568,实际为{len(preds)}"
    # 生成时间戳
    timestamps = pd.date_range(
        start="2025-01-01",
        periods=5568,
        freq="15T"
    ).strftime("%Y/%m/%d %H:%M")
    # 创建DataFrame
    df = pd.DataFrame({
        "": timestamps,
        "power": preds[:5568]  # 确保只取5568个点
    })
    # 保存文件
    output_path = f"output/output{station_id}.csv"
    df.to_csv(output_path, index=False)
    print(f"场站 {station_id} 预测结果已保存,共 {len(preds)} 个数据点")
def main():
    try:

 

 

总结

   由于清洗数据采用的baseline 对于异常值跳过的操作,导致原本就已经少量的数据集对于transformer架构更加稀少,在预测时对于NAN值则直接补零使最终效果也就差强人意,后续优化方向可以对于数据清洗上进行生成均值为数据集中均值与方差进行填充操作。

 

国家电网调控AI创新大赛中的一个赛题是新能源发电预测。该赛题旨在探索人工智能技术在电网运行中的应用,通过智能算法提升电力预测调度能力。参赛者可以利用各种数据和技术手段,设计出能够精准预测新能源发电情况的算法。这个赛题的目标是为电网运营提供可靠的信息,帮助调度人员更好地安排电网运行,实现智能能源的有效利用。该赛题的设置旨在促进人工智能技术在电力调控领域的创新应用,推动电力系统的智能化发展。通过这个赛题,希望能够提高新能源发电预测的准确性和可靠性,为电网运行提供更高效的调度方案。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [阿里云:用AI让中国“制造”变“智造”|Xtecher特写](https://blog.youkuaiyun.com/weixin_33704591/article/details/90594359)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [国家电网调控人工智能创新大赛开启 百度飞桨提供国产AI平台](https://blog.youkuaiyun.com/weixin_45449540/article/details/119673531)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值