헤매어도 한 걸음씩

[논문 리뷰] Are Transformers Effective for Time Series Forecasting? 본문

Paper Review

[논문 리뷰] Are Transformers Effective for Time Series Forecasting?

ritz 2021. 7. 18. 14:51
원문 : https://arxiv.org/abs/2205.13504

 

Abstract

최근 들어 Time Series Forecasting(TSF), 특히 Long Term TSF에서 Transformer 기반의 모델이 다수 제안됨

Transformer 기반의 모델은 Paired Element 사이에 존재하는 상관관계를 추출하는 것에 뛰어나지만, 'Permutation-Invariant' 즉 순서를 고려하지 않는다는 단점이 있음.

→ 시계열 예측 문제의 경우 연속적인 순서로 이루어진 데이터로부터 Temporal Dynamics 를 포착하는 것이 매우 중요한 바, Transformer 기반의 모델이 Long-Term TSF 문제에 적합한지에 대해 다시 생각해볼 필요가 있음

트랜스포머 모델이 명확한 추세와 주기성을 지닌 장기 시계열 예측에 정말 효과적인가?

 

→ 간단한 선형 분해 모델로도 높은 성능을 낼 수 있음

(일부 Long-Sequence Time Series Forecasting에서 SOTA를 달성)

 

CONCEPT

Transformer Based Model

 

LTSF 과제에서는 시간적 변화를 모델링하는데 주 목적을 두기 때문에 시간 순차성 정보가 예측에 있어 가장 중요한 역할을 하게됨

 

트랜스포머의 경우 이런 시간 순차성 정보를 보존하기 위해 positional encoding 기법을 사용하였지만 인코딩하는 과정과 다음에 진행되는 multi-head self-attention 적용 후 시간 순차성에 대한 정보 손실을 피할 수는 없음

 

일반적으로 트랜스포머 모델이 주로 사용되는 NLP 분야에서는 문장 내 단어의 순서가 어느 정도 바뀌어도 의미 자체가 크게 변하지 않기 때문에(ex. 나는 배가 고프다, 배가 고프다 나는) 큰 문제가 발생하지 않지만, 추세와 주기성을 지닌 수치 시계열 데이터에서는 의미 정보가 부족하기 때문에 이와 같은 정보 손실은 큰 문제를 야기시킬 수 있음

 

실제로 트랜스포머 모델의 경우 look-back window sizes(=추적 기간) 를 증가시켜도 예측 오류가 감소(때로는 증가)하지 않는 현상을 보고 시간적인 관계에 대한 특징을 추출하지 못한다는 것을 발견

 

따라서 LTSF 과제에서 트랜스포머 모델의 성능이 다소 과장되었다고 생각되며, 장기 시계열 예측에 정말 효과적인가에 대한 의문이 제기

LookBackWindow Test

 

위와 같은 이유로 본 논문에서는 간단한 선형 모델이 장기 시계열 데이터에서 시간 순차성 정보를 그대로 보존하면서 추세와 주기성에 대한 특징을 보다 잘 추출할 수 있기 때문에 LTSF-Linear 모델을 제안

 

해당 모델은 간단한 단일 선형 레이어로 구성되었지만 9개의 벤치마크 데이터셋에서 기전의 트랜스포머 기반 모델보다 뛰어난 성능을 보임

 

 

 

STRUCTURE

: 3개의 LTSF-Linear 모델 소개

1) Linear

  • Linear: 단 하나의 선형 레이어로 구성된 모델이지만 트랜스포머 모델보다 성능이 우수

class LTSF_Linear(torch.nn.Module):
    def __init__(self, window_size, forcast_size, individual, feature_size):
        super(LTSF_Linear, self).__init__()
        self.window_size = window_size
        self.forcast_size = forcast_size
        self.individual = individual
        self.channels = feature_size
        if self.individual:
            self.Linear = torch.nn.ModuleList()
            for i in range(self.channels):
                self.Linear.append(torch.nn.Linear(self.window_size, self.forcast_size))
        else:
            self.Linear = torch.nn.Linear(self.window_size, self.forcast_size)

    def forward(self, x):
        if self.individual:
            output = torch.zeros([x.size(0),self.pred_len,x.size(2)],dtype=x.dtype).to(x.device)
            for i in range(self.channels):
                output[:,:,i] = self.Linear[i](x[:,:,i])
            x = output
        else:
            x = self.Linear(x.permute(0,2,1)).permute(0,2,1)
        return x

 

2) DLinear

  • Autoformer와 FEDformer에서 사용되는 시계열 분해 방식을 선형 레이어와 결합한 모델
  • 먼저 이동 평균값을 만들고 이를 제거하여 각각 학습하기 위해 추세와 주기성 데이터로 분해
  • 그 다음 각 구성 요소에 단일 선형 레이어를 적용하여 학습하고 두 개를 합산하여 최종 예측을 계산
  • Dlinear는 시계열 데이터의 명확한 추세와 주기성이 있을 때 더 나은 성능을 가질 수 있음

 

# 우선 입력 시계열 데이터를 Trend와 Seasonality로 분해
class moving_avg(nn.Module):
    """
    Moving average block to highlight the trend of time series
    """
    def __init__(self, kernel_size, stride):
        super(moving_avg, self).__init__()
        self.kernel_size = kernel_size
        self.avg = nn.AvgPool1d(kernel_size=kernel_size, stride=stride, padding=0)

    def forward(self, x):
        # padding on the both ends of time series
        front = x[:, 0:1, :].repeat(1, (self.kernel_size - 1) // 2, 1)
        end = x[:, -1:, :].repeat(1, (self.kernel_size - 1) // 2, 1)
        x = torch.cat([front, x, end], dim=1)
        x = self.avg(x.permute(0, 2, 1))
        x = x.permute(0, 2, 1)
        return x


class series_decomp(nn.Module):
    """
    Series decomposition block
    """
    def __init__(self, kernel_size):
        super(series_decomp, self).__init__()
        self.moving_avg = moving_avg(kernel_size, stride=1)

    def forward(self, x):
        moving_mean = self.moving_avg(x)
        res = x - moving_mean
        return res, moving_mean


## 분해된 입력을 각각 1-Layer Linear Network에 통과시켜 예측 결과를 얻는 모델을 구현
# 1-layer linear network 구현 부분
class Model(nn.Module):
    """
    DLinear
    """
    def __init__(self, configs):
        super(Model, self).__init__()
        self.seq_len = configs.seq_len
        self.pred_len = configs.pred_len

        # Decompsition Kernel Size
        kernel_size = 25
        self.decompsition = series_decomp(kernel_size)
        self.individual = configs.individual
        self.channels = configs.enc_in

        if self.individual:
            self.Linear_Seasonal = nn.ModuleList()
            self.Linear_Trend = nn.ModuleList()
            self.Linear_Decoder = nn.ModuleList()
            for i in range(self.channels):
                self.Linear_Seasonal.append(nn.Linear(self.seq_len,self.pred_len))
                self.Linear_Seasonal[i].weight = nn.Parameter((1/self.seq_len)*torch.ones([self.pred_len,self.seq_len]))
                self.Linear_Trend.append(nn.Linear(self.seq_len,self.pred_len))
                self.Linear_Trend[i].weight = nn.Parameter((1/self.seq_len)*torch.ones([self.pred_len,self.seq_len]))
                self.Linear_Decoder.append(nn.Linear(self.seq_len,self.pred_len))
        else:
            self.Linear_Seasonal = nn.Linear(self.seq_len,self.pred_len)
            self.Linear_Trend = nn.Linear(self.seq_len,self.pred_len)
            self.Linear_Decoder = nn.Linear(self.seq_len,self.pred_len)
            self.Linear_Seasonal.weight = nn.Parameter((1/self.seq_len)*torch.ones([self.pred_len,self.seq_len]))
            self.Linear_Trend.weight = nn.Parameter((1/self.seq_len)*torch.ones([self.pred_len,self.seq_len]))

    def forward(self, x):
        # x: [Batch, Input length, Channel]
        seasonal_init, trend_init = self.decompsition(x)
        seasonal_init, trend_init = seasonal_init.permute(0,2,1), trend_init.permute(0,2,1)
        if self.individual:
            seasonal_output = torch.zeros([seasonal_init.size(0),seasonal_init.size(1),self.pred_len],dtype=seasonal_init.dtype).to(seasonal_init.device)
            trend_output = torch.zeros([trend_init.size(0),trend_init.size(1),self.pred_len],dtype=trend_init.dtype).to(trend_init.device)
            for i in range(self.channels):
                seasonal_output[:,i,:] = self.Linear_Seasonal[i](seasonal_init[:,i,:])
                trend_output[:,i,:] = self.Linear_Trend[i](trend_init[:,i,:])
        else:
            seasonal_output = self.Linear_Seasonal(seasonal_init)
            trend_output = self.Linear_Trend(trend_init)

        x = seasonal_output + trend_output
        return x.permute(0,2,1) # to [Batch, Output length, Channel]

 

 

Experiments

  1. Dataset : 전기 변압기 온도 예측 데이터셋
  2. Evaluation Metric : MSE, MAE
  3. 실험 결과

 

 

해석

위 그림의 (a)와 (b)는 Exchange Rate 데이터에 대한 결과 (추세와 주기성 가중치 확인)

 

금융 시계열 데이터의 경우 주기성과 계절성이 거의 존재하지 않기 때문에 일정한 패턴을 보이지는 않지만, Trend Layer를 통해 Output에 가까운 정보들이 높은 Weight를 가지고 있는 것을 보며, 해당 데이터들이 예측값에 많은 기여를 하고 있음을 확인할 수 있음

 

또한 (c)와 (d)의 경우 Electricity 데이터에 대한 결과로 24step을 주기로 동일한 패턴이 반복되는 것을 볼 수 있는데 이는 해당 데이터가 하루 전력 사용량에 대한 데이터이고, 하루가 24시간 이기 때문

 

매우 간단한 선형 모델이기 때문에 DLinear의 Weight W는 모델이 어떻게 동작하고 있는지에 대한 직관적인 해석을 제공할 수 있음

DLinear는 다음과 같은 수식으로 표현

(1) X^=Hs+Ht (각각의 H는 Weight W와 분해된 입력 X가 곱해진 형태)

(2) Remainder  Hs=WsXs

(3) Trend  Ht=WtXt

(모든 Variate가 같은 Linear Layer를 공유하는 형태를 DLinear-S라 하고, 각각의 Variate가 각기 다른 Linear Layer를 가지는 형태를 DLinear-I 라고 함 -기본적으로는 DLinear-S를 사용)

 

효율성

Multiply-Accumulate Operation(MACs), 파라미터의 수, 시간 복잡도와 메모리 측면에서 DLinear은 Transformer 기반의 모델들에 비해 효율적임을 확인

 

Conclusion

 

최근 주요 학회들에서 Transformer 기반의 모델들이 시계열 예측 문제에 대한 해결 방안으로 발표되었으며 일부는 Best Paper로 채택까지 되었으나, 자연어 처리 분야에서 주로 사용되는 Transformer를 굳이 시계열 예측 문제에 사용하는 것이 효율적인가에 대한 근본적인 질문을 던지는 논문

 

즉, 필요 없이 복잡하게 만들지 말 것!