又一開發者公佈高分方案源代碼,助力軟件杯選手高效解題

本文作者:艾宏峯

  • 算法工程師

  • M6 Global賽道總排名4th

  • KDD Cup 2022風電功率預測飛槳賽道5th

“中國軟件杯”大學生軟件設計大賽——龍源風電賽道,5月31日預選賽截止,80%選手將晉級區域賽,歡迎大家抓緊報名

賽題背景

隨着清潔能源的快速發展,風力發電已經成爲可再生能源的重要組成部分,然而風具有隨機性特點,常規天氣預報無法準確反映出風電場所在區域的真實風速,從而造成發電功率預測準確率低下,影響電力供需平衡。因此,提高風電功率預測的準確性,爲電網調度提供科學支撐,對我國能源產業有十分重要的價值。

此次賽題由百度飛槳和龍源電力設置,數據集由全球最大風電運營企業龍源電力提供,採集自真實風力發電數據,要求選手基於百度飛槳 PaddlePaddle 設計一個可以通過深度學習技術實現對風力發電進行功率預測及管理的軟件系統。

時序預測技術介紹

時間序列預測技術是指基於歷史數據和時間變化規律,通過數學模型和算法對未來發展趨勢進行預測的一種技術。時間序列預測技術廣泛應用於經濟、金融、交通、氣象等領域,以幫助人們做出更加準確的決策。
時序預測從不同角度看有不同分類:

  • 實現原理的角度,可以分爲:傳統統計學,機器學習(又分非深度學習和深度學習);

  • 預測步長區分,可以分爲單步預測和多步預測;

  • 輸入變量區分,可以分爲自迴歸預測和使用協變量進行預測;

  • 輸出結果區分,可以分爲點預測和概率預測;

  • 目標個數區分,可以分爲一元、多元、多重時間序列預測

這些分類是不同角度下的分類,同一種算法往往只能是分類中的一種,例如傳統的統計學算法只適合做自迴歸預測而不適合協變量預測。

時間序列預測技術的研究歷史可以追溯到20世紀初期。最早的時間序列預測方法是基於時間平均法和線性趨勢法,後來發展出了指數平滑法、ARIMA 模型、神經網絡模型等預測方法。隨着機器學習和深度學習的發展,時間序列預測技術也得到了不斷的拓展和創新,比如 Transformer 與時序的結合。

風電功率預測研究意義與價值

時間序列預測技術有着廣泛的應用場景。例如,

  • 經濟領域,時間序列預測技術可以用於股票市場預測、經濟增長預測、通貨膨脹預測等。

  • 交通領域,時間序列預測技術可以用於公交車到站時間預測、交通擁堵預測等。

  • 氣象領域,時間序列預測技術可以用於氣象災害預警、天氣變化預測等。

而在風電功率預測上,其研究意義和價值更不容忽視,它們包括:

  • 提高風電發電效率:通過精準預測風電功率,可以合理安排風電機組的運行,提高風電發電效率,減少能源浪費。

  • 保障電網穩定運行:風電功率預測可以幫助電網運營商及時調整電網負荷,避免電網過載或供電不足等問題,保障電網穩定運行。

  • 促進可再生能源發展:風電功率預測可以提高風電發電的可靠性和經濟性,促進可再生能源發展,降低對傳統能源的依賴。

  • 降低能源成本:通過精準預測風電功率,可以有效避免風電機組的過剩和不足,降低能源成本,提高能源利用率。

  • 推動智能電網建設:風電功率預測是智能電網建設的重要組成部分,可以實現對風電發電的精準監控和管理,推動智能電網建設。

賽制賽段

  • 預選賽

5月31日截止,算法賽,80%選手晉級區域賽;

  • 區域賽

6-7月,算法賽60%+軟件賽40%,頒發省級獎項;

  • 總決賽

8月,軟件賽,頒發國賽獎項。

賽題數據

本賽題數據集由全球最大風電運營企業龍源電力提供,採集自真實風力發電數據。預選賽訓練數據和區域賽訓練數據分別爲不同10個風電場近一年的運行數據共30萬餘條,每15分鐘採集一次,包括風速、風向、溫度、溼度、氣壓和真實功率等,具體的數據字段中英文對應如下:

  • WINDSPEED 預測風速

  • WINDDIRECTION 風向

  • TEMPERATURE 溫度

  • HUMIDITY 溼度

  • PRESSURE 氣壓

  • PREPOWER 預測功率(系統生成)

  • ROUND(A.WS,1)實際風速

  • ROUND(A.POWER,0) 實際功率(計量口徑一)

  • YD15 實際功率(預測目標,計量口徑二)

注:“預測風速”字段,指的是由權威的氣象機構,像是中央氣象臺、歐洲國家氣象中心等發佈的商業氣象數據源。從時間線來說,實際功率預測需要提前 36 個小時、72 個小時、240 的小時等獲得數值天氣預報,從而進行功率的預測。

數據注意事項

  • 原始數據集存在不同格式的風機數據,需要額外的數據拼接處理工作;

  • 每個風機的最後一天的 ROUND(A.POWER,0)和 YD15 兩個字段數據基本爲空,這是出題方希望我們預測填空的數據;

  • csv 內的時間戳未必有序,需要自行排序;

  • 數據存在缺失和離羣值;

  • 數據存在重複樣本(即同一時間戳有多條樣本)。以風機04爲例,重複樣本數量: 34764, 佔比: 38.68340232340766%。

以風機4爲例的EDA結果展示

官方對數據的一些回覆

  • 由於測量設備和網絡傳輸問題,YD15 可能出現數據異常(包括用於評測的輸入數據)。

  • 實際上由於一些髒數據的存在,YD15 有時候會缺失或異常,這個時候 ROUND(A.POWER,0)如果有正常值的話,可以被視爲YD15的替代。

  • YD15 的異常值處理規則是,當 YD15 爲空時,按照邏輯依次用 ROUND(A.POWER,0)、PREPOWER 進行替換。

  • 如何定義 YD15 存在異常?在本賽題中,YD15 異常包括兩種情況:(1) 空值,(2) 在一段時間內、其它字段正常變化時,YD15 持續完全不變。除以上兩種情況之外,YD15 的數值變化都可認爲是正常現象,如爲 0 或負值。

個人補充

  • 當實際風速爲 0 時,存在功率>0 有些異常,然後有些風速過大>12.5,存在功率爲 0 的異常。

  • 目標列 ROUND(A.POWER,0)和 YD15,與風速 WINDSPEED、PREPOWER、PRESSURE 和 ROUND(A.WS,1)強相關;

  • 目標列 ROUND(A.POWER,0)和 YD15 之間就有很強的相關性;

評測說明

算法部分

本次比賽要求選手將算法模型提交至人工智能學習與實訓社區 AI Studio 進行自動評測,預選賽開放10個風場數據,區域賽開放新的10個風場數據,預選賽和區域賽算法成績各佔30%。

要求選手基於飛槳 PaddlePaddle 根據官方提供的數據集,設計一種利用當日05:00之前的數據,預測次日00:00至23:45實際功率的方法。準確率按日統計,根據10個風電場平均準確率進行排名;準確率相同的情形下,根據每日單點的平均最大偏差絕對值排名。

def calc_acc(y_true, y_pred):
    rmse = np.sqrt(np.mean((y_true - y_pred)**2))
    return 1 - rmse/201000

軟件部分

本次比賽要求選手基於 Web 技術實現:

  • 數據可視化

將預測結果以圖表等形式展示出來,便於用戶進行觀察和分析;

  • 實時更新與滾動預測

能夠基於提供的數據實時模擬真實功率、預測功率及其之間的差異,通過調節過去不同長度的時間段,以更新未來預測結果,且預測的時間段可調節;

  • 響應式設計

支持多種終端,包括 PC 端、移動端等,以適應不同設備的屏幕尺寸和分辨率;

  • 其他創新附加功能

提交說明

測評數據的格式如下:

| --- ./infile(內置於測評系統中,參賽選手不可見)
     | --- 0001in.csv
     | --- 0002in.csv
     | ---  ...

參賽選手需要提交一個命名爲 submission.zip 的壓縮包,並且壓縮包內應包含:

| --- ./model # 存放模型的目錄,並且大小不超過200M(必選)
| --- ./env # 存放依賴庫的目錄(可選)
| --- predict.py # 評估代碼(必選)
| --- pip-requirements.txt # 存放依賴庫的文件(可選)
| --- …

參賽選手在代碼提交頁面提交壓縮包後,測評系統會解壓選手提交的壓縮包,並執行如下命令:

python predict.py

測評文件 predict.py 應該完成的功能是:讀取 infile 文件夾下的測評數據,並將預測結果保存到 pred 文件夾中。

| --- ./pred(需要選手生成)
     | --- 0001out.csv
     | --- 0002out.csv
     | --- …

基線模型流程

基於飛槳 PaddlePaddle 的多任務 LSTM 時序預測基線模型 pipeline 如下:

以時間序列舉例,因爲一般測試集也會是未來數據,所以我們也要保證訓練集是歷史數據,而劃分出的驗證集是未來數據,不然會發生“時間穿越”的數據泄露問題,導致模型過擬合(例如用未來預測歷史數據),這個時候就有兩種驗證劃分方式可參考使用:

  • TimeSeriesSplit:Sklearn 提供的 TimeSeriesSplit;

  • 固定窗口滑動劃分法:固定時間窗口,不斷在數據集上滑動,獲得訓練集和驗證集。(個人推薦這種)

在時序任務中,有2類數據源,如下圖所示:

(1)靜態變量(Static Covariates):不會隨時間變化的變量,例如風機id、風機位置;

(2)時變變量(Time-dependent Inputs):隨時間變化的變量;

  • 過去觀測的時變變量(Past-observed Inputs):過去可知,但未來不可知,例如歷史風速、溫度、氣壓等

  • 先驗已知未來的時變變量(Apriori-known Future Inputs):過去和未來都可知,例如天氣預報未來風速、溫度、氣壓等;

數據加載器的代碼如下:

# unix時間戳轉換
def to_unix_time(dt):
    # timestamp to unix
    epoch = datetime.datetime.utcfromtimestamp(0)
    return int((dt - epoch).total_seconds())

def from_unix_time(unix_time):
    # unix to timestamp
    return datetime.datetime.utcfromtimestamp(unix_time)

class TSDataset(paddle.io.Dataset):
    """時序DataSet
    劃分數據集、適配dataloader所需的dataset格式
    ref: https://github.com/thuml/Autoformer/blob/main/data_provider/data_loader.py
    """
    def __init__(self, data, 
                 ts_col='DATATIME',
                 use_cols =['WINDSPEED', 'PREPOWER', 'WINDDIRECTION', 'TEMPERATURE', 'HUMIDITY', 
                 'PRESSURE', 'ROUND(A.WS,1)', 'ROUND(A.POWER,0)', 'YD15',
                 'month', 'day', 'weekday', 'hour', 'minute'],
                 labels = ['ROUND(A.POWER,0)', 'YD15'], 
                 input_len = 24*4*5, pred_len = 24*4, stride=19*4, data_type='train',
                 train_ratio = 0.7, val_ratio = 0.15):
        super(TSDataset, self).__init__()
        self.ts_col = ts_col        # 時間戳列
        self.use_cols = use_cols    # 訓練時使用的特徵列
        self.labels = labels        # 待預測的標籤列
        self.input_len = input_len  # 模型輸入數據的樣本點長度,15分鐘間隔,一個小時14個點,近5天的數據就是24*4*5
        self.pred_len = pred_len    # 預測長度,預測次日00:00至23:45實際功率,即1天:24*4
        self.data_type = data_type  # 需要加載的數據類型
        self.scale = True           # 是否需要標準化
        self.train_ratio = train_ratio # 訓練集劃分比例
        self.val_ratio = val_ratio  # 驗證集劃分比例
        # 由於賽題要求利用當日05:00之前的數據,預測次日00:00至23:45實際功率
        # 所以x和label要間隔19*4個點
        self.stride = stride
        assert data_type in ['train', 'val', 'test']    # 確保data_type輸入符合要求
        type_map = {'train': 0, 'val': 1, 'test': 2}
        self.set_type = type_map[self.data_type]

        self.transform(data)

    def transform(self, df):
        # 獲取unix時間戳、輸入特徵和預測標籤
        time_stamps, x_values, y_values = df[self.ts_col].apply(lambda x:to_unix_time(x)).values, df[self.use_cols].values, df[self.labels].values
        # 劃分數據集
        # 這裏可以按需設置劃分比例
        num_train = int(len(df) * self.train_ratio)
        num_vali = int(len(df) * self.val_ratio)
        num_test = len(df) - num_train - num_vali
        border1s = [0, num_train-self.input_len-self.stride, len(df)-num_test-self.input_len-self.stride]
        border2s = [num_train, num_train + num_vali, len(df)]
        # 獲取data_type下的左右數據截取邊界
        border1 = border1s[self.set_type]
        border2 = border2s[self.set_type]    

        # 標準化
        self.scaler = StandardScaler()
        if self.scale:
            # 使用訓練集得到scaler對象
            train_data = x_values[border1s[0]:border2s[0]]
            self.scaler.fit(train_data)
            data = self.scaler.transform(x_values)
            # 保存scaler
            pickle.dump(self.scaler, open('/home/aistudio/submission/model/scaler.pkl', 'wb'))
        else:
            data = x_values

        # array to paddle tensor
        self.time_stamps = paddle.to_tensor(time_stamps[border1:border2], dtype='int64')
        self.data_x = paddle.to_tensor(data[border1:border2], dtype='float32')
        self.data_y = paddle.to_tensor(y_values[border1:border2], dtype='float32')  

    def __getitem__(self, index):
        """
        實現__getitem__方法,定義指定index時如何獲取數據,並返回單條數據(訓練數據)
        """
        # 由於賽題要求利用當日05:00之前的數據,預測次日00:00至23:45實際功率
        # 所以x和label要間隔19*4個點
        s_begin = index
        s_end = s_begin + self.input_len
        r_begin = s_end + self.stride
        r_end = r_begin + self.pred_len

        # TODO 可以增加對未來可見數據的獲取
        seq_x = self.data_x[s_begin:s_end]
        seq_y = self.data_y[r_begin:r_end]
        ts_x = self.time_stamps[s_begin:s_end]
        ts_y = self.time_stamps[r_begin:r_end]
        return seq_x, seq_y, ts_x, ts_y

    def __len__(self):
        """
        實現__len__方法,返回數據集總數目
        """
        return len(self.data_x) - self.input_len - self.stride - self.pred_len  + 1


class TSPredDataset(paddle.io.Dataset):
    """時序Pred DataSet
    劃分數據集、適配dataloader所需的dataset格式
    ref: https://github.com/thuml/Autoformer/blob/main/data_provider/data_loader.py
    """
    def __init__(self, data, 
                 ts_col='DATATIME',
                 use_cols =['WINDSPEED', 'PREPOWER', 'WINDDIRECTION', 'TEMPERATURE', 'HUMIDITY', 
                 'PRESSURE', 'ROUND(A.WS,1)', 'ROUND(A.POWER,0)', 'YD15',
                 'month', 'day', 'weekday', 'hour', 'minute'],
                 labels = ['ROUND(A.POWER,0)', 'YD15'],  
                 input_len = 24*4*5, pred_len = 24*4, stride=19*4):
        super(TSPredDataset, self).__init__()
        self.ts_col = ts_col        # 時間戳列
        self.use_cols = use_cols    # 訓練時使用的特徵列
        self.labels = labels        # 待預測的標籤列
        self.input_len = input_len  # 模型輸入數據的樣本點長度,15分鐘間隔,一個小時14個點,近5天的數據就是24*4*5
        self.pred_len = pred_len    # 預測長度,預測次日00:00至23:45實際功率,即1天:24*4
        # 由於賽題要求利用當日05:00之前的數據,預測次日00:00至23:45實際功率
        # 所以x和label要間隔19*4個點
        self.stride = stride        
        self.scale = True           # 是否需要標準化

        self.transform(data)

    def transform(self, df):
        # 獲取unix時間戳、輸入特徵和預測標籤
        time_stamps, x_values, y_values = df[self.ts_col].apply(lambda x:to_unix_time(x)).values, df[self.use_cols].values, df[self.labels].values
        # 截取邊界
        border1 = len(df) - self.input_len - self.stride - self.pred_len
        border2 = len(df)   

        # 標準化
        self.scaler = StandardScaler()
        if self.scale:
            # 讀取預訓練好的scaler
            self.scaler = pickle.load(open('/home/aistudio/submission/model/scaler.pkl', 'rb'))
            data = self.scaler.transform(x_values)
        else:
            data = x_values

        # array to paddle tensor
        self.time_stamps = paddle.to_tensor(time_stamps[border1:border2], dtype='int64')
        self.data_x = paddle.to_tensor(data[border1:border2], dtype='float32')
        self.data_y = paddle.to_tensor(y_values[border1:border2], dtype='float32')  

    def __getitem__(self, index):
        """
        實現__getitem__方法,定義指定index時如何獲取數據,並返回單條數據(訓練數據)
        """
        # 由於賽題要求利用當日05:00之前的數據,預測次日00:00至23:45實際功率
        # 所以x和label要間隔19*4個點
        s_begin = index
        s_end = s_begin + self.input_len
        r_begin = s_end + self.stride
        r_end = r_begin + self.pred_len

        # TODO 可以增加對未來可見數據的獲取
        seq_x = self.data_x[s_begin:s_end]
        seq_y = self.data_y[r_begin:r_end]
        ts_x = self.time_stamps[s_begin:s_end]
        ts_y = self.time_stamps[r_begin:r_end]
        return seq_x, seq_y, ts_x, ts_y

    def __len__(self):
        """
        實現__len__方法,返回數據集總數目
        """
        return len(self.data_x) - self.input_len - self.stride - self.pred_len  + 1

模型代碼如下:

class MultiTaskLSTM(paddle.nn.Layer):
    """多任務LSTM時序預測模型
    LSTM爲共享層網絡,對兩個預測目標分別有兩個分支獨立線性層網絡

    TODO 其實該模型就是個Encoder,如果後續要引入天氣預測未來的變量,補充個Decoder,
    然後Encoder負責歷史變量的編碼,Decoder負責將 編碼後的歷史編碼結果 和 它編碼未來變量的編碼結果 合併後,做解碼預測即可
    """
    def __init__(self,feat_num=14, hidden_size=64, num_layers=2, dropout_rate=0.7, input_len=120*4, pred_len=24*4):
        super(MultiTaskLSTM, self).__init__()
        # LSTM爲共享層網絡
        self.lstm_layer = paddle.nn.LSTM(feat_num, hidden_size, 
                                    num_layers=num_layers, 
                                    direction='forward', 
                                    dropout=dropout_rate)
        # 爲'ROUND(A.POWER,0)'構建分支網絡
        self.linear1_1 = paddle.nn.Linear(in_features=input_len*hidden_size, out_features=hidden_size*2)
        self.linear1_2 = paddle.nn.Linear(in_features=hidden_size*2, out_features=hidden_size)
        self.linear1_3 = paddle.nn.Linear(in_features=hidden_size, out_features=pred_len)
        # 爲'YD15'構建分支網絡 
        self.linear2_1 = paddle.nn.Linear(in_features=input_len*hidden_size, out_features=hidden_size*2)
        self.linear2_2 = paddle.nn.Linear(in_features=hidden_size*2, out_features=hidden_size)
        self.linear2_3 = paddle.nn.Linear(in_features=hidden_size, out_features=pred_len)
        self.dropout = paddle.nn.Dropout(dropout_rate)


    def forward(self, x):
        # x形狀大小爲[batch_size, input_len, feature_size]
        # output形狀大小爲[batch_size, input_len, hidden_size]
        # hidden形狀大小爲[num_layers, batch_size, hidden_size]
        output, (hidden, cell) = self.lstm_layer(x)
        # output: [batch_size, input_len, hidden_size] -> [batch_size, input_len*hidden_size]
        output = paddle.reshape(output, [len(output), -1])

        output1 = self.linear1_1(output)
        output1 = self.dropout(output1)
        output1 = self.linear1_2(output1)
        output1 = self.dropout(output1)
        output1 = self.linear1_3(output1)

        output2 = self.linear2_1(output)
        output2 = self.dropout(output2)
        output2 = self.linear2_2(output2)
        output2 = self.dropout(output2)
        output2 = self.linear2_3(output2)

        # outputs: ([batch_size, pre_len, 1], [batch_size, pre_len, 1])
        return [output1, output2]

模型訓練、驗證和測試代碼:

def train(df, turbine_id):
    # 設置數據集
    train_dataset = TSDataset(df, input_len = input_len, pred_len = pred_len, data_type='train')
    val_dataset = TSDataset(df, input_len = input_len, pred_len = pred_len, data_type='val')
    test_dataset = TSDataset(df, input_len = input_len, pred_len = pred_len, data_type='test')
    print(f'LEN | train_dataset:{len(train_dataset)}, val_dataset:{len(val_dataset)}, test_dataset:{len(test_dataset)}')

    # 設置數據讀取器
    train_loader = paddle.io.DataLoader(train_dataset, shuffle=True, batch_size=batch_size, drop_last=True)
    val_loader = paddle.io.DataLoader(val_dataset, shuffle=False, batch_size=batch_size, drop_last=True)
    test_loader = paddle.io.DataLoader(test_dataset, shuffle=False, batch_size=1, drop_last=False)

    # 設置模型
    model = MultiTaskLSTM()

    # 設置優化器
    scheduler = paddle.optimizer.lr.ReduceOnPlateau(learning_rate=learning_rate, factor=0.5, patience=3, verbose=True)
    opt = paddle.optimizer.Adam(learning_rate=scheduler, parameters=model.parameters())

    # 設置損失
    mse_loss = MultiTaskMSELoss()

    train_loss = []
    valid_loss = []
    train_epochs_loss = []
    valid_epochs_loss = []
    early_stopping = EarlyStopping(patience=patience, verbose=True, ckp_save_path=f'/home/aistudio/submission/model/model_checkpoint_windid_{turbine_id}.pdparams')

    for epoch in tqdm(range(epoch_num)):
        # =====================train============================
        train_epoch_loss, train_epoch_mse1,  train_epoch_mse2 = [], [], []
        model.train() # 開啓訓練
        for batch_id, data in enumerate(train_loader()):             
            x = data[0]
            y = data[1]
            # 預測
            outputs = model(x)
            # 計算損失
            mse1, mse2, avg_loss = mse_loss(outputs, y)
            # 反向傳播
            avg_loss.backward()
            # 梯度下降
            opt.step()
            # 清空梯度
            opt.clear_grad()
            train_epoch_loss.append(avg_loss.numpy()[0])
            train_loss.append(avg_loss.item())
            train_epoch_mse1.append(mse1.item())
            train_epoch_mse2.append(mse2.item())
        train_epochs_loss.append(np.average(train_epoch_loss))
        print("epoch={}/{} of train | loss={}, MSE of ROUND(A.POWER,0):{}, MSE of YD15:{} ".format(epoch, epoch_num,
        np.average(train_epoch_loss), np.average(train_epoch_mse1), np.average(train_epoch_mse2)))

        # =====================valid============================
        model.eval() # 開啓評估/預測
        valid_epoch_loss, valid_epochs_mse1,  valid_epochs_mse2 = [], [], []
        for batch_id, data in enumerate(val_loader()): 
            x = data[0]
            y = data[1]
            outputs = model(x)
            mse1, mse2, avg_loss = mse_loss(outputs, y)
            valid_epoch_loss.append(avg_loss.numpy()[0])
            valid_loss.append(avg_loss.numpy()[0])
            valid_epochs_mse1.append(mse1.item())
            valid_epochs_mse2.append(mse2.item())
        valid_epochs_loss.append(np.average(valid_epoch_loss))
        print('Valid: MSE of ROUND(A.POWER,0):{}, MSE of YD15:{}'.format(np.average(train_epoch_mse1), np.average(train_epoch_mse2)))

        # ==================early stopping======================
        early_stopping(valid_epochs_loss[-1], model=model)
        if early_stopping.early_stop:
            print(f"Early stopping at Epoch {epoch-patience}")
            break

    print('Train & Valid: ')
    plt.figure(figsize=(12,3))
    plt.subplot(121)
    plt.plot(train_loss[:],label="train")
    plt.title("train_loss")
    plt.xlabel('iteration')
    plt.subplot(122)
    plt.plot(train_epochs_loss[1:],'-o',label="train")
    plt.plot(valid_epochs_loss[1:],'-o',label="valid")
    plt.title("epochs_loss")
    plt.xlabel('epoch')
    plt.legend()
    plt.tight_layout()
    plt.show()

    # =====================test============================
    # 加載最優epoch節點下的模型
    model = MultiTaskLSTM()
    model.set_state_dict(paddle.load(f'/home/aistudio/submission/model/model_checkpoint_windid_{turbine_id}.pdparams'))

    model.eval() # 開啓評估/預測
    test_loss, test_epoch_mse1, test_epoch_mse2 = [], [], []
    test_accs1, test_accs2 = [], [] 
    for batch_id, data in tqdm(enumerate(test_loader())): 
        x = data[0]
        y = data[1]
        ts_y = [from_unix_time(x) for x in data[3].numpy().squeeze(0)]
        outputs = model(x)
        mse1, mse2, avg_loss = mse_loss(outputs, y)
        acc1 = calc_acc(y.numpy().squeeze(0)[:,0], outputs[0].numpy().squeeze(0))
        acc2 = calc_acc(y.numpy().squeeze(0)[:,1], outputs[1].numpy().squeeze(0))
        test_loss.append(avg_loss.numpy()[0])
        test_epoch_mse1.append(mse1.numpy()[0])
        test_epoch_mse2.append(mse2.numpy()[0])
        test_accs1.append(acc1)
        test_accs2.append(acc2)

    print('Test: ')
    print('MSE of ROUND(A.POWER,0):{}, MSE of YD15:{}'.format(np.average(test_epoch_mse1), np.average(test_epoch_mse2)))
    print('Mean MSE:', np.mean(test_loss))
    print('ACC of ROUND(A.POWER,0):{}, ACC of YD15:{}'.format(np.average(test_accs1), np.average(test_accs2)))

更多代碼,詳見:

  • 龍源風電賽Baseline - 多任務LSTM深度網絡模型 (Paddle)

https://aistudio.baidu.com/aistudio/projectdetail/5911966?contributionType=1&sUid=397884&shared=1&ts=1683175097635

提分技巧

挖掘風機間關聯信息

雖然賽方沒給出不同風機的地理位置,且不同風機的數據分佈時間段不完全不一致,但可以從風場維度,聯合多個風機開展數據分析,看是否能挖掘出風機之間的關聯信息,以減少單風機的數據噪聲、幫助填補缺失或處理異常等。

還有例如求功率/溫度相關性,Kmeans 聚類獲取風機 cluster,把近鄰風機的特徵們求均值加入特徵,或者分 cluster 建模預測。

ST-Tree module:ST-Tree:Spatio-Partitioned Time-Phased Tree Model。它算每個風機之間的皮爾遜係數,再用 K-means 聚類,然後針對聚類的風機訓練 LightGBM。 --KDD 第1名方案(海康)

  • 在圖網絡表徵上,選手是把與風機有 Top-K 高相關性的風機們,確認連接邊,構建圖關係。

  • 用 k-shape 算法將風機聚類成39類,每類風機用一個 LightGBM 預測。

  • 附屬風機可被看做是一類風機們,它們之間有最相似的發電規律。通過平均附屬風機的功率,當做特徵加入模型,能緩解數據噪聲。

--KDD 第4名方案(清華、浙江工商、多倫多大學)</p>

分段建模預測

風機的發電功率預測難度是挺大的,模型一般偏好均值預測,所以我們要爭取把可預測性強的短期預測部分預測好,再考慮如何提升模型的中長期預測能力。其實短期預測可以考慮遞歸預測,因爲它強調時序前後依賴,對短期信息依賴程度會更高些,不足在於中後期會誤差累積。所以在中長期則可以考慮多步生成式預測。

論文《DeepSpatio-TemporalWind Power Forecasting》反應到:

  • 隨着時間滯後項的增加,自相關係數迅速衰減,說明短期預測可能性更大,中長期就比較困難了。

  • 隨機抽取幾個不同的風速時序,結果顯示風速時序沒有展示出極端的長期依賴。而且 GRU 用更少的參數反而避免了過擬合問題。

下圖展示了頭幾個小時下的 MAE,也反映出相同的結論。所以風電功率預測時大多用戶選擇了分段建模預測。

其他

  • 異常值處理,尤其是標籤矯正: 例如風速過大但功率爲0的異常,在特定風速下的離羣功率等;

  • 標籤融合: 融合兩個標籤,以 YD15 爲主,A.Power 爲輔;

  • 利用天氣預報的數據: 加 decoder 部分加入即可; 

  • 挖掘更多特徵: 差分序列、同時刻風場/鄰近風機的特徵均值/標準差等;

  • 嘗試樹模型: XGB、LGB 等;

  • 模型參數調優: optuna;

  • 模型融合: 人工加權或配合尋參算法。

官方交流QQ羣

QQ 搜索479266219加入官方交流 QQ 羣~👇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章