Transformer を使用した時系列の予測 (PyTorch コードに基づく)

コードソース

https://github.com/nok-halfspace/Transformer-Time-Series-Forecasting

文章情報:https://medium.com/mlearning-ai/transformer-implementation-for-time-series-forecasting-a9db2db5c820

データ構造

このプロジェクトのデータ構造は次のようになります。異なるセンサー ID があり、これらのセンサーは異なる期間で異なる湿度を持ちます。 

データのインポートと前処理

最初のステップは、データの前処理を実行することです。DataLoader のコードは次のとおりです。

class SensorDataset(Dataset):
    """Face Landmarks dataset."""

    def __init__(self, csv_name, root_dir, training_length, forecast_window):
        """
        Args:
            csv_file (string): Path to the csv file.
            root_dir (string): Directory
        """
        
        # load raw data file
        csv_file = os.path.join(root_dir, csv_name)
        self.df = pd.read_csv(csv_file)
        self.root_dir = root_dir
        self.transform = MinMaxScaler() #对数据进行归一化处理
        self.T = training_length
        self.S = forecast_window

    def __len__(self):
        # return number of sensors
        return len(self.df.groupby(by=["reindexed_id"]))

    # Will pull an index between 0 and __len__. 
    def __getitem__(self, idx):
        
        # Sensors are indexed from 1
        idx = idx+1

        # np.random.seed(0)

        start = np.random.randint(0, len(self.df[self.df["reindexed_id"]==idx]) - self.T - self.S) 
        sensor_number = str(self.df[self.df["reindexed_id"]==idx][["sensor_id"]][start:start+1].values.item())
        index_in = torch.tensor([i for i in range(start, start+self.T)])
        index_tar = torch.tensor([i for i in range(start + self.T, start + self.T + self.S)])
        _input = torch.tensor(self.df[self.df["reindexed_id"]==idx][["humidity", "sin_hour", "cos_hour", "sin_day", "cos_day", "sin_month", "cos_month"]][start : start + self.T].values)
        target = torch.tensor(self.df[self.df["reindexed_id"]==idx][["humidity", "sin_hour", "cos_hour", "sin_day", "cos_day", "sin_month", "cos_month"]][start + self.T : start + self.T + self.S].values)

        # scalar is fit only to the input, to avoid the scaled values "leaking" information about the target range.
        # scalar is fit only for humidity, as the timestamps are already scaled
        # scalar input/output of shape: [n_samples, n_features].
        scaler = self.transform

        scaler.fit(_input[:,0].unsqueeze(-1))
        _input[:,0] = torch.tensor(scaler.transform(_input[:,0].unsqueeze(-1)).squeeze(-1))
        target[:,0] = torch.tensor(scaler.transform(target[:,0].unsqueeze(-1)).squeeze(-1))

        # save the scalar to be used later when inverse translating the data for plotting.
        dump(scaler, 'scalar_item.joblib')

        return index_in, index_tar, _input, target, sensor_number

より重要な点の 1 つは、初期データが MinMaxScaler() によって処理されること、つまり、ディープ ラーニングでは非常に一般的な操作であるデータ正規化が実行されることです。​ 

時刻情報の埋め込み

LSTM モデルとは異なり、トランス モデルではすべての情報が一度に投入されるため、時間情報はありません。したがって、時系列の場合は、時間情報の追加処理が必要になります。次のコードでは、sin_hour、cos_hour、sin_day、cos_day などの情報が元のデータ セットに追加されます。これは、位置埋め込みメカニズムに似ています。表現する情報を以下の図に示します。

import pandas as pd
import time
import numpy as np
import datetime
from icecream import ic

# encoding the timestamp data cyclically. See Medium Article.
def process_data(source):

    df = pd.read_csv(source)
        
    timestamps = [ts.split('+')[0] for ts in  df['timestamp']]
    timestamps_hour = np.array([float(datetime.datetime.strptime(t, '%Y-%m-%d %H:%M:%S').hour) for t in timestamps])
    timestamps_day = np.array([float(datetime.datetime.strptime(t, '%Y-%m-%d %H:%M:%S').day) for t in timestamps])
    timestamps_month = np.array([float(datetime.datetime.strptime(t, '%Y-%m-%d %H:%M:%S').month) for t in timestamps])

    hours_in_day = 24
    days_in_month = 30
    month_in_year = 12

    df['sin_hour'] = np.sin(2*np.pi*timestamps_hour/hours_in_day)
    df['cos_hour'] = np.cos(2*np.pi*timestamps_hour/hours_in_day)
    df['sin_day'] = np.sin(2*np.pi*timestamps_day/days_in_month)
    df['cos_day'] = np.cos(2*np.pi*timestamps_day/days_in_month)
    df['sin_month'] = np.sin(2*np.pi*timestamps_month/month_in_year)
    df['cos_month'] = np.cos(2*np.pi*timestamps_month/month_in_year)

    return df

train_dataset = process_data('Data/train_raw.csv')
test_dataset = process_data('Data/test_raw.csv')

train_dataset.to_csv(r'Data/train_dataset.csv', index=False)
test_dataset.to_csv(r'Data/test_dataset.csv', index=False)

次に、このコード スニペットを通じて、より多くの変数を含む新しいデータを取得します。 

データは後続の main.py で正規化されているため、このデータセットでは元の湿度データの正規化プロセスを見ることができないことに注意してください。

Transformer モデルに取り込んで計算します

データを処理した後の次のステップは、計算のためにデータをモデルに取り込むことです。

変圧器モデルの定義

これはこのプロジェクトにおいて非常に重要なコードセグメントであるため、ここで分析する必要があります。コードの最初で、作成者は、この計算が「必要なのは注意だけです」という記事に基づいていると提案しました。​ 

トランスフォーマはエンコーダ、デコーダ、フィードフォワードで構成されます。

モデル構築: このプロジェクトでは、すべての既知の履歴データを使用して、将来の期間のデータを予測します。 X1 ~ X5 をそれぞれ過去の期間 1 ~ 5 の履歴データとすると、X2 を予測する場合は、X2、X3、…のデータのみを使用します。​ 

マスクされたセルフアテンション: トランスフォーマーではセルフ アテンション メカニズムが使用されていますが、予測プロセスでは予測期間以前のデータを見ることができないため、ここではマスクされたメカニズムが使用されています。簡単に言うと、将来のデータを設定します。モデルがそれ以降の値を認識できないように、非常に小さな数値に設定します。​ 

Pytorch には既製のコード セグメントがすでに存在するため、それを使用するにはここでパラメーターを定義するだけで済みます。簡単そうに聞こえますが、使用中に多くのトラブルに遭遇することがよくあります。​ 

  • feature_size: 使用される特徴の数。このプロジェクトでは、時間の 6 つの特徴 ('sin_hour'、'cos_hour'、'sin_day'、') を指します。 ;cos_day', 'sin_month', 'cos_month')+1 初期データ
  • num_layers: エンコーダー層の数これはモデルに従って具体的に調整できます。
  • ドロップアウト: モデルに応じて特別に調整できます。
  • nhead: 多層アテンション メカニズムのヘッドの数。特徴の数はヘッドの数で割り切れなければならないことに注意してください。そうでない場合、モデルはエラーを報告します (この点は理解しやすいです。ヘッドの数は分割マッピングの数に相当します (割り切れない場合は分割することが困難です)。
import torch.nn as nn
import torch, math
from icecream import ic
import time
"""
The architecture is based on the paper “Attention Is All You Need”. 
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Lukasz Kaiser, and Illia Polosukhin. 2017.
"""

class Transformer(nn.Module):
    # d_model : number of features
    def __init__(self,feature_size=7,num_layers=3,dropout=0):
        super(Transformer, self).__init__()

        self.encoder_layer = nn.TransformerEncoderLayer(d_model=feature_size, nhead=7, dropout=dropout)
        self.transformer_encoder = nn.TransformerEncoder(self.encoder_layer, num_layers=num_layers)        
        self.decoder = nn.Linear(feature_size,1) #feature_size是input的个数,1为output个数
        self.init_weights()
    
    #init_weight主要是用于设置decoder的参数
    def init_weights(self):
        initrange = 0.1    
        self.decoder.bias.data.zero_()
        self.decoder.weight.data.uniform_(-initrange, initrange)

    def _generate_square_subsequent_mask(self, sz):
        mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
        mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
        return mask

    def forward(self, src, device):
        
        mask = self._generate_square_subsequent_mask(len(src)).to(device)
        output = self.transformer_encoder(src,mask)
        output = self.decoder(output)
        return output

実行モデル

このモデルでは、batch_size が 1 に設定されています。これは、このモデルでは各センサーが互いに独立しており、それぞれの計算に変圧器モデルが使用されることを意味します。各センサー間の関係は考慮されていません。​ 

import argparse
# from train_teacher_forcing import *
from train_with_sampling import *
from DataLoader import *
from torch.utils.data import DataLoader
import torch.nn as nn
import torch
from helpers import *
from inference import *

def main(
    epoch: int = 1000,
    k: int = 60,
    batch_size: int = 1,
    frequency: int = 100,
    training_length = 48,
    forecast_window = 24,
    train_csv = "train_dataset.csv",
    test_csv = "test_dataset.csv",
    path_to_save_model = "save_model/",
    path_to_save_loss = "save_loss/", 
    path_to_save_predictions = "save_predictions/", 
    device = "cpu"
):

    clean_directory()

    train_dataset = SensorDataset(csv_name = train_csv, root_dir = "Data/", training_length = training_length, forecast_window = forecast_window)
    train_dataloader = DataLoader(train_dataset, batch_size=1, shuffle=True)
    test_dataset = SensorDataset(csv_name = test_csv, root_dir = "Data/", training_length = training_length, forecast_window = forecast_window)
    test_dataloader = DataLoader(test_dataset, batch_size=1, shuffle=True)

    best_model = transformer(train_dataloader, epoch, k, frequency, path_to_save_model, path_to_save_loss, path_to_save_predictions, device)
    inference(path_to_save_predictions, forecast_window, test_dataloader, device, path_to_save_model, best_model)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--epoch", type=int, default=1000)
    parser.add_argument("--k", type=int, default=60)
    parser.add_argument("--batch_size", type=int, default=1)
    parser.add_argument("--frequency", type=int, default=100)
    parser.add_argument("--path_to_save_model",type=str,default="save_model/")
    parser.add_argument("--path_to_save_loss",type=str,default="save_loss/")
    parser.add_argument("--path_to_save_predictions",type=str,default="save_predictions/")
    parser.add_argument("--device", type=str, default="cpu")
    args = parser.parse_args()

    main(
        epoch=args.epoch,
        k = args.k,
        batch_size=args.batch_size,
        frequency=args.frequency,
        path_to_save_model=args.path_to_save_model,
        path_to_save_loss=args.path_to_save_loss,
        path_to_save_predictions=args.path_to_save_predictions,
        device=args.device,
    )

おすすめ

転載: blog.csdn.net/weixin_44897685/article/details/133828710