Time Series Forecasting with Deep Learning (LSTMs)
1. Introduction
Although I am not convinced by the efficiency of using Deep Learning for this particular competition, I still wanted to give it a go to see what we could get from it. I checked other existing notebooks for this given competition but couldn’t find any finalised notebooks. So here is an attempt to use LSTMs for Time Series Forecasting. Before you read further, let me mention a few points:
- the LSTM model was not able to beat my baseline model (very simplistic approach using the values in the last week of the training set). I have more tricks up my sleeve but wasn’t able to try them yet. If you think of any other ideas please let me know.
- this approach can only work if all the pairs store-family in the test set are also in the training set. This check was made in another notebook so I won’t duplicate the code here:
- the code implementation was inspired from this tensorflow tutorial:
비록 이 대회에서 딥 러닝을 사용하는 효율성에 대해 난감한 생각이 들긴 하지만, 여전히 이를 시도하여 무엇을 얻을 수 있는지 확인해보고자 합니다. 이 대회와 관련된 다른 노트북을 확인해보았지만, 마무리된 노트북을 찾지 못했습니다. 그래서 LSTM을 사용한 시계열 예측을 시도해보았습니다. 더 읽기 전에, 몇 가지 지적할 점을 말씀드리겠습니다.
- LSTM 모델은 베이스라인 모델(매우 단순한 방법으로 train set 마지막 주의 값 사용)보다 우수한 성능을 보이지 못했습니다. 하지만 더 많은 아이디어가 있으니 시도해볼 계획입니다. 다른 생각이 떠오르면 제게 알려주세요.
- 이 접근법은 테스트 세트의 모든 store-family 쌍이 훈련 세트에도 포함된 경우에만 작동할 수 있습니다. 이 검사는 다른 노트북에서 수행되었으므로 여기에서 중복 코드를 작성하지 않겠습니다.
- 코드 구현은 이 TensorFlow 튜토리얼에서 영감을 받았습니다.
2. Loading data and libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import random
from datetime import datetime, timedelta
from tqdm import tqdm
from sklearn.preprocessing import MinMaxScaler
import os
import tensorflow as tf
- numpy : 파이썬에서 수치 계산을 위한 라이브러리입니다.
- pandas : 파이썬에서 데이터를 다루기 위한 라이브러리입니다.
- matplotlib.pyplot : 파이썬에서 그래프를 그리기 위한 라이브러리입니다.
- seaborn : matplotlib를 기반으로 한 시각화 라이브러리입니다.
- random : 파이썬에서 난수를 생성하기 위한 라이브러리입니다.
- datetime : 파이썬에서 날짜와 시간을 다루기 위한 라이브러리입니다.
- timedelta : 파이썬에서 날짜와 시간 간격을 다루기 위한 라이브러리입니다.
- tqdm : 파이썬에서 for loop의 진행상태를 표시하는 라이브러리입니다.
- sklearn.preprocessing : scikit-learn 라이브러리에서 데이터 전처리를 위한 라이브러리입니다.
- os : 파이썬에서 운영체제 기능을 사용하기 위한 라이브러리입니다.
- tensorflow : 딥러닝 모델을 구현하기 위한 라이브러리입니다.
df_holidays = pd.read_csv('holidays_events.csv', encoding = 'cp949', header = 0, parse_dates = ['date'])
df_oil = pd.read_csv("oil.csv", encoding = 'cp949', header = 0, parse_dates = ['date'])
df_stores = pd.read_csv("stores.csv", encoding = 'cp949', header = 0)
df_trans = pd.read_csv("transactions.csv", encoding = 'cp949', header = 0, parse_dates = ['date'])
df_train = pd.read_csv("train.csv", encoding = "cp949", header = 0, parse_dates = ['date'])
df_test = pd.read_csv("test.csv", encoding = "cp949", header = 0, parse_dates = ['date'])
- parse_dates 는 read_csv 함수의 옵션 중 하나로, CSV 파일에서 날짜와 시간 정보를 읽어들이기 위해 사용됩니다. 이 옵션에는 날짜 정보가 있는 열의 이름을 지정할 수 있습니다.
li = [df_holidays, df_oil, df_stores, df_trans, df_train, df_test]
for i in range(len(li)):
if 'sales' in li[i].columns:
li[i].rename(columns = {'sales': 'y'}, inplace = True)
if 'date' in li[i].columns:
li[i].rename(columns = {'date' : 'ds'}, inplace = True)
- inplace = True 파라미터를 설정하면 rename() 함수가 데이터프레임을 직접 변경합니다.
def rmsle(y_hat, y):
""" Compute Root Mean Squared Logarithmic Error """
metric = np.sqrt(sum((np.array(list(map(lambda x : np.log(x + 1), y_hat)))
- np.array(list(map(lambda x : np.log(x + 1), y)))) ** 2) / len(y))
return round(metric, 4)
- rmsle 함수는 Root Mean Squared Logarithmic Error (RMSLE)를 계산하는 함수입니다. 이 함수는 예측값과 실제값의 차이를 로그를 취하여 계산하고, 그 값에 대해 Root Mean Squared Error (RMSE)를 계산하는 방식으로 작동합니다. RMSLE는 종종 회귀 문제에서 예측 성능을 평가하기 위해 사용되는 지표 중 하나입니다. 즉, 이 함수는 예측값 y_hat 과 실제값 y 간의 RMSLE를 계산하여 반환합니다. 반환되는 값은 예측 성능의 지표가 되며, 값이 작을수록 더 좋은 예측 성능을 나타냅니다.
3. Utils
class WindowGenerator():
def __init__(self, input_width, label_width, shift, train_df, val_df, test_df, label_columns = None):
# Store the raw data.
self.train_df = train_df
self.val_df = val_df
self.test_df = test_df
# Work out the label column indices.
self.label_columns = label_columns
if label_columns is not None:
self.label_columns_indices = {name: i for i, name in enumerate(label_columns)}
self.column_indices = {name: i for i, name in enumerate(train_df.columns)}
# Work out the window parameters.
self.input_width = input_width
self.label_width = label_width
self.shift = shift
self.total_window_size = input_width + shift
self.input_slice = slice(0, input_width)
self.input_indices = np.arange(self.total_window_size)[self.input_slice]
self.label_start = self.total_window_size - self.label_width
self.labels_slice = slice(self.label_start, None)
self.label_indices = np.arange(self.total_window_size)[self.labels_slice]
def __repr__(self):
return '\n'.join([
f'Total window size: {self.total_window_size}',
f'Input indices: {self.input_indices}',
f'Label indices: {self.label_indices}',
f'Label column name(s): {self.label_columns}'
])
- 위 코드는 TensorFlow의 WindowGenerator 클래스를 정의하는 코드입니다. 이 클래스는 시계열 데이터를 입력받아, 입력 시퀀스와 레이블 시퀀스를 처리하고 모델링에 필요한 형태로 변환하는 클래스입니다.
- 클래스의 생성자 메소드는 다음과 같은 파라미터를 받습니다.
- input_width : 입력 시퀀스의 길이를 나타내는 정수입니다.
- label_width : 레이블 시퀀스의 길이를 나타내는 정수입니다.
- shift : 레이블 시퀀스가 시작되는 시점을 나타내는 정수입니다.
- train_df, val_df, test_df : 각각 훈련, 검증, 테스트 데이터셋을 나타내는 Pandas DataFrame입니다.
- label_columns : 레이블 데이터의 컬럼 이름을 나타내는 리스트입니다.
- 클래스의 속성으로는 다음과 같은 것이 있습니다.
- train_df, val_df, test_df : 생성자에서 입력으로 받은 데이터셋을 나타내는 Pandas DataFrame입니다.
- column_indices : 데이터셋의 컬럼 이름과 인덱스를 매핑한 딕셔너리입니다.
- label_columns_indices : 레이블 데이터의 컬럼 이름과 인덱스를 매핑한 딕셔너리입니다.
- input_width, label_width, shift : 생성자에서 입력으로 받은 하이퍼파라미터입니다.
- total_window_size : 입력 시퀀스와 레이블 시퀀스를 포함한 윈도우 크기입니다.
- input_slice, input_indices : 입력 시퀀스를 나타내는 슬라이스와 인덱스입니다.
- labels_slice, label_indices : 레이블 시퀀스를 나타내는 슬라이스와 인덱스입니다.
- 또한 클래스에는 ‘repr()’ 메소드가 구현되어 있습니다. 이 메소드는 클래스의 속성들을 문자열로 반환하여 출력해줍니다.
- 입력 시퀀스(Input Sequence)는 시계열 데이터에서 모델에 입력되는 부분으로, 이전 시간 단계에서 관찰된 값을 타나냅니다. 입력 시퀀스는 모델의 입력으로 사용되어 예측을 수행하는 데 필요한 정보를 제공합니다.
- 레이블 시퀀스(Label Sequence)는 모델이 예측해야 하는 값의 시퀀스입니다. 레이블 시퀀스는 모델의 출력으로 사용되어 모델이 예측하는 데 필요한 목표값을 제공합니다.
def split_window(self, features):
inputs = features[:, self.input_slice, :]
labels = features[:, self.labels_slice, :]
if self.label_columns is not None:
labels = tf.stack(
[labels[:, :, self.column_indices[name]] for name in self.label_columns], axis = -1)
# slicing doesn't preserve static shape information, so set the shapes
# manually. This way the 'tf.data.Datasets' are easier to inspect.
inputs.set_shape([None, self.input_width, None])
labels.set_shape([None, self.label_width, None])
- 해당 코드는 TensorFLow를 이용하여 시계열 데이터를 처리하기 위해 사용되는 유틸리티 클래스인 “WindowGenerator” 클래스의 일부입니다.
- 이 메소드는 “WindowGenerator” 클래스 내에서 정의된 인풋 및 라벨 슬라이스를 이용하여 시계열 데이터를 인풋 데이터와 라벨 데이터로 분리하는 역할을 합니다.
- 먼저 features 인자는 sahpe가 (batch_size, num_features)인 2D 텐서입니다. 이 때, num_features는 시계열 데이터에서 추출된 특징(feature)의 개수를 의미합니다.
- 그리고, ‘self.input_slice’ 및 ‘self.labels_slice’는 각각 인풋과 라벨로 사용될 feature들의 인덱스 범위를 나타내는 tuple입니다.
- 위에서 정의된 인풋 및 라벨 슬라이스를 이용하여 features를 분리하고, 이를 inputs 및 labels 변수에 할당합니다.
- 그 다음, self.label_columns 이 None이 아니라면 라벨 데이터를 label_columns 으로 지정된 열 (column)들만 선택하도록 처리합니다. 이때, self.column_indices 딕셔너리를 이용하여 각 열의 인덱스를 가져옵니다. 결과적으로 labels는 (batch_size, label_width, num_label_columns) 형태의 3D 텐서가 됩니다.
- 마지막으로, inputs 및 labels 의 shape 정보를 설정합니다. 입력 데이터의 shape는 (batch_size, input_width, num_input_features)이고 라벨 데이터의 shape는 (batch_size, label_width, num_label_columns)입니다. slicing 작업은 shape 정보를 유지하지 않기 때문에, set_shape() 함수를 사용하여 shape 정보를 수동으로 설정합니다. 이렇게 하면 tf.data.Datasets를 더 쉽게 검사할 수 있습니다.
def plot(self, model = None, plot_col = 'T (degC)', max_subplots = 3):
inputs, labels = self.example
plt.figure(figsize = (12, 8))
plot_col_index = self.column_indices[plot_col]
max_n = min(max_subplots, len(inputs))
for n in range(max_n):
plt.subplot(max_n, 1, n + 1)
plt.ylabel(f'{plot_col}[normed]')
plt.plot(self.input_indices, inputs[n, :, plot_col_index],
label = 'Inputs', marker = '.', zorder = -10)
if self.label_columns:
label_col_index = self.label_columns_indices.get(plot_col, None)
else:
label_col_index = plot_col_index
if label_col_index is None:
continue
plt.scatter(self.label_indices, labels[n, :, label_col_index], edgecolors = 'k', label = 'Labels', c = '#2ca02c', s = 64)
if model is not None:
predictions = model(inputs)
plt.scatter(self.label_indices, predictions[n, :, label_col_index],
marker = 'X', edgecolors = 'k', label = 'Predictions', c = '#ff7f0e', s = 64)
if n == 0:
plt.legend()
plt.xlabel('Days')
- 해당 코드는 TensorFlow를 이용하여 시계열 데이터를 처리하기 위해 사용되는 “WindowGenerator” 클래스의 메소드 중 하나로, 시계열 데이터의 예측 결과를 시각화하기 위한 함수입니다.
- 이 메소드는 ‘plot_col’로 지정된 열(column)의 값을 그래프로 출력합니다. 또한, max_subplots 값에 따라 그래프의 개수를 제한할 수 있습니다.
- 먼저, example 변수에서 입력 데이터와 라벨 데이터를 가져옵니다. 이 때 inputs는 (num_samples, input_width, num_features)의 shape를 가지는 3D 텐서이며, labels는 (num_samples, label_width, num_features)의 shape을 가지는 3D 텐서입니다.
- 그리고, plot_col로 지정된 열의 인덱스를 plot_col_index 변수에 할당합니다. 이 인덱스는 column_indices 딕셔너리를 이용하여 구할 수 있습니다.
- 다음으로, 출력할 그래프의 개수를 결정합니다. max_subplots 값과 입력 데이터의 개수 (num_samples) 중 작은 값을 선택하여 max_n 변수에 할당합니다.
- 그 다음, max_n 개수만큼 그래프를 출력합니다. subplot() 함수를 이용하여 subplot을 생성하고, plot() 함수를 이용하여 입력 데이터를 출력합니다.
- 그리고, label_columns이 None이 아니라면, plot_col로 지정된 열이 라벨 데이터 중 어떤 열에 해당하는지를 label_columns_indices 딕셔너리를 이용해 확인합니다. 이 때, label_columns_indices 딕셔너리에 plot_col이 없다면, 해당 열은 라벨 데이터에 없는 것으로 판단하고 다음 그래프로 넘어갑니다.
- 만약 label_col_index가 None이 아니라면, scatter() 함수를 이용하여 라벨 데이터를 점으로 표시합니다. 이 때, edgecolors, label, c, s 등의 인자를 이용하여 점의 스타일을 설정합니다.
- 또한, model 인자가 None이 아니라면, 입력 데이터를 모델에 입력하여 예측 결과를 가져옵니다. 이 예측 결과를 scatter() 함수를 이용하여 점으로 표시합니다.
- 마지막으로, 첫 번째 그래프에는 범례(legend)를 표시합니다. 그리고 x 축에는 Days를, y축에는 plot_col을 나타내는 라벨을 설정합니다.
def make_dataset(self, data):
data = np.array(data, dtype = np.float32)
ds = tf.keras.utils.timeseries_dataset_from_array(data = data, targets = None, sequence_length = self.total_window_size,
sequence_stride = 1, shuffle = True, batch_size = 32, )
ds = ds.map(self.split_window)
return ds
- make_dataset 메소드는 입력 데이터를 tf.data.Dataset 형식으로 변환하여 반환하는 메소드입니다. 이 메소드는 입력 데이터를 np.array 형식으로 받습니다.
- tf.keras.utils.timeseries_dataset_from_array 함수를 사용하여 입력 데이터를 시계열 데이터셋으로 변환합니다. sequence_length 인자를 통해 시퀀스의 길이를 설정하고 sequence_stride 인자를 통해 시퀀스 간격을 설정합니다. shuffle 인자를 True로 설정하면 데이터가 무작위로 섞이게 됩니다. batch_size 는 데이터를 처리할 때 사용할 배치 크기를 설정합니다.
- 이후, ds.map(self.split_window)를 사용하여 데이터셋을 자르는 split_window 메소드를 적용합니다. split_window 메소드에서는 입력과 레이블을 분리하고, 레이블을 사용자가 지정한 레이블 컬럼에 대해서만 선택적으로 추출합니다.
- 마지막으로 변환된 tf.data.Dataset 을 반환합니다.
@property
def train(self):
return self.make_dataset(self.train_df)
@property
def val(self):
return self.make_dataset(self.val_df)
@property
def test(self):
return self.make_dataset(self.test_df)
@property
def examplt(self):
""" Get and cache an example batch of 'inputs, labels' for plotting. """
result = getattr(self, '_example', None)
if result is None:
# No example batch was found, so get one from the '.train' dataset
result = next(iter(self.train))
# And cache it for next time
self._example = result
return result
- 위 코드는 @property 데코레이터를 사용하여 train, val, test, example 속성을 정의합니다.
- train, val, test 는 각각 학습, 검증, 테스트 데이터셋을 반환합니다. 이 속성들은 make_dataset 메소드를 호출하여 입력 데이터를 tf.data.Dataset 형식으로 변환하고, 이를 반환합니다.
- example 속성은 플로팅을 위한 예제 데이터셋을 반환합니다. 이 속성은 getattr 함수를 사용하여 _example 속성이 있는지 확인하고, 없으면 학습 데이터셋에서 한 배치의 데이터를 가져와 _example 속성에 저장합니다. 그리고 _example 속성을 반환합니다. getattr 함수는 객체의 속성을 가져오는데 사용됩니다. getattr(self, ‘_example’, None)에서 self는 현재 인스턴스를 의미하며, _example 속성이 없는 경우 None 값을 반환합니다.
4. Baseline model - last week value
기준 모델 - 지난 주 값
It is always good to start with the a baseline model before writing complex models. In one of my previous notebook, I used the last year value as a baseline model (see here for more details:https://www.kaggle.com/loicge/sales-top-down-approach-with-prophet-0-52). To make things a bit different this time, I decided to use the last avilable week value. Let’s see what we get.
복잡한 모델을 작성하기 전에 기준 모델로 시작하는 것이 항상 좋습니다. 이전 노트북 중 하나에서는 작년 값(~)을 기준 모델을 사용했습니다. 이번에는 조금 다르게 사용 가능한 일주일 값으로 기준 모델을 사용하기로 결정했습니다. 이를 통해 얻는 결과를 확인해보겠습니다.
def get_last_available_week(df):
# Get date of the last available week
df = df.assign(diff_from_max_train_ds = df.ds - (df.ds.min() - timedelta(days = 1))) # -datetime.strptime(df.ds.min(), "%Y-%m-%d")
df = df.assign(nb_weeks = np.ceil(df.diff_from_max_train_ds.dt.days / 7).astype('int'))
df = df.assign(last_week_ds = df.ds - (df.nb_weeks * 7).map(lambda x: timedelta(x)))
return df
- 이 함수는 데이터프레임에서 마지막으로 사용 가능한 주의 날짜를 가져오는데 사용됩니다. 함수의 첫 번째 인자로 데이터프레임이 전달되면, 이 데이터프레임에 대해 다음과 같은 작업을 수행합니다.
- 먼저, ‘ds’ 컬럼의 최솟값에서 1일을 뺀 값으로부터 ‘ds’컬럼의 날짜와의 차이를 계산하여 ‘diff_from_max_train_ds’라는 새로운 컬럼을 생성합니다. 그 다음, 이 값을 7로 나눈 후 올림하여 정수로 형변환합니다. 이 값은 데이터프레임에서 마지막으로 사용 가능한 주의 개수를 의미합니다.
- 마지막으로, 이 값을 이용하여 마지막으로 사용 가능한 주의 시작 날짜를 계산합니다. 이를 위해 ds 값에서 주 수에 7을 곱한 값을 뺀 후, 이 값을 이용하여 timedelta 객체를 생성합니다. 이를 last_week_ds 라는 새로운 컬럼으로 저장하고, 이를 반환합니다.
def get_yhat(df):
df = pd.merge(df,
df[['ds', 'store_nbr', 'family', 'prop_family_per_store']].rename(columns = {'prop_family_per_store' : 'last_week_prop_family_per_store'}),
left_on = ['last_week_ds', 'store_nbr', 'family'],
right_on = ['ds', 'store_nbr', 'family'],
how = 'left').drop(['diff_from_max_train_ds', 'nb_weeks', 'last_week_ds', 'ds_y'], axis = 1).rename(columns = {'ds_x': 'ds'})
df = df.assign(yhat = df.yhat_store_nbr * df.last_week_prop_family_per_store)
return df
- 이 코드는 기본적으로 예측 값을 계산하는 함수입니다. 먼저 df 데이터프레임에 대해 last_week_ds 를 계산합니다. 그리고 이전 주 데이터를 활용하여 각 가게와 제품군별 상품 판매 비율을 구합니다. 이후 df 데이터프레임과 조인하여 last_week_prop_family_per_store 열을 추가합니다. 이 값은 해당 가게와 제품군의 상품 판매 비율입니다.
- 마지막으로 yhat 열을 계산합니다. yhat는 yhat_store_nbr(가게별 예측값)과 last_week_prop_family_per_store을 곱한 값입니다. 따라서 이 함수는 가게와 가족별 판매 비율을 고려한 예측 값을 계산하는 데 사용됩니다.
train_df, test_df = df_train.copy(), df_test.copy()
# Cross validation
val_df = train_df[(train_df.ds >= '2017-08-01') & (train_df.ds <= '2017-08-15')]
val_df = pd.merge(get_last_available_week(val_df)[['id', 'ds', 'last_week_ds', 'store_nbr', 'family', 'y']],
train_df[['ds', 'store_nbr', 'family', 'y']],
left_on = ['last_week_ds', 'store_nbr', 'family'], right_on = ['ds', 'store_nbr', 'family'], how = 'left').rename(columns = {'ds_x': 'ds', 'y_x': 'y', 'y_y': 'yhat'})
print('RMSLE: %s'% rmsle(val_df.yhat, val_df.y))
# 출력값 RMSLE: 0.6124
- 위 코드는 데이터프레임(df_train)을 학습 데이터(train_df)와 검증 데이터(val_df), 테스트 데이터(test_df)로 나누는 작업을 수행합니다. 그리고 검증 데이터(val_df)를 활용하여 크로스 밸리데이션(cross validation)을 수행하고, 이를 통해 예측 결과의 평가 지표인 RMSLE(Root Mean Squared Logarithmic Error) 값을 출력합니다. RSMLE는 예측 값과 실제 값의 차이를 로그 스케일로 변환하여 계싼한 평균 제곱근 오차(RMSE)입니다. 이 값은 예측 모델의 성능을 평가하는데 사용되는 일반적인 지표 중 하나입니다.
We obtain a score of 0.6124 which is actually much better than the score obtained with the last year value model on the same validation period (0.9495). Let’s now submit on the leaderboard and see what we get.
submission_df = pd.merge(get_last_available_week(test_df)[['id', 'ds', 'last_week_ds', 'store_nbr', 'family']],
train_df[['ds', 'store_nbr', 'family', 'y']],
left_on = ['last_week_ds', 'store_nbr', 'family'], right_on = ['ds', 'store_nbr', 'family'], how = 'left')[['id', 'y']].rename(columns = {'y': 'sales'})
# submission_df.to_csv('submission.csv', index = False)
- 위 코드는 test_df 데이터프레임을 기반으로 예측한 결과를 제출용 데이터프레임 submission_df 로 저장하는 코드입니다.
Nice! The score on the leaderboard is 0.52245 which is not bad at all for a baseline model. I would assume that it performs better than the last year value model because the last available week value reflects more the recent data (such as trend or seasonality, especially weekly seasonality).
좋아요! 리더보드에서의 점수는 0.52245로, 베이스라인 모델로는 꽤 괜찮은 성능을 보이는 것 같습니다. 작년 값 모델보다 더 좋은 성능을 발휘하는 것으로 추측됩니다. 왜냐하면 마지막으로 사용 가능한 주 값은 최근 데이터를 더 잘 반영하기 때문입니다.
5. LSTM Model
5.1 Training
Now that we have a baseline model, let’s try to beat that model using LSTMs.
이제 베이스라인 모델이 준비되었으니, LSTMs를 사용하여 이 모델을 개선해보려고 합니다.
# Parameters
start_training_ds = '2017-01-01'
input_width = 1 * 7
label_width = 16
MAX_EPOCHS = 50
learning_rate = 0.001
scaling = ['standardisation', 'normalisation'][0]
# Reshape the dataframe
df = df_train[df_train.ds >= start_training_ds].assign(key = df_train['store_nbr'].astype('str') + '~' + df_train['family'])
df = pd.pivot_table(df, values = 'y', index = ['ds'], columns = 'key').reset_index()
date_time = df.ds
df = df.drop('ds', axis = 1)
df = df.iloc[:, :df.shape[1]]
column_indices = {name: i for i, name in enumerate(df.columns)}
# Split into train, val and test set
n = len(df)
test_df = df[-label_width:]
val_df = df[-(input_width + 2 * label_width):-label_width]
train_df = df[:-(input_width + label_width)]
print("Train set size: (%s, %s)"%(train_df.shape[0], train_df.shape[1]))
print("Validation set size: (%s, %s)"%(test_df.shape[0], test_df.shape[1]))
print("Test set size: (%s, %s) \n"%(val_df.shape[0], val_df.shape[1]))
num_features = df.shape[1]
# Perform normalisation
if scaling == 'standardisation':
train_mean = train_df.mean()
train_std = train_df.std()
train_df = (train_df - train_mean) / train_std
train_df = train_df.fillna(0)
val_df = (val_df - train_mean) / train_std
val_df = val_df.fillna(0)
test_df = (test_df - train_mean) / train_std
test_df = test_df.fillna(0)
train_df[train_df == np.inf] = 0
val_df[val_df == np.inf] = 0
test_df[test_df == np.inf] = 0
elif scaling == 'normalisation':
scaler = MinMaxScaler(feature_range = (0, 1))
train_df = pd.DataFrame(scaler.fit_transform(train_df), columns = train_df.columns)
val_df = pd.DataFrame(scaler.transform(val_df), columns = val_df.columns)
test_df = pd.DataFrame(scaler.transform(test_df), columns = test_df.columns)
WindowGenerator.train = train
WindowGenerator.val = val
WindowGenerator.test = test
WindowGenerator.example = example
WindowGenerator.make_dataset = make_dataset
WindowGenerator.split_window = split_window
WindowGenerator.plot = plot
# Generate windows for training batches
window = WindowGenerator(input_width = input_width, label_width = label_width, shift = label_width, train_df = train_df, val_df = val_df, test_df = test_df)
model = tf.keras.Sequential([
# Shape [batch, time, features] => [batch, lstm_units].
tf.keras.layers.Conv1D(filters = 128, kernel_size = (input_width,), activation = 'relu'),
tf.keras.layers.LSTM(128, return_sequences = False),
tf.keras.layers.Dense(label_width * num_features, kernel_initializer = tf.initializers.zeros()),
tf.keras.layers.Reshape([label_width, num_features])
])
print('Input shape:', window.example[0].shape)
print('Labels shape:', window.example[1].shape)
print('Output shape:', model(window.example[0]).shape)
history = compile_and_fit(model, window, MAX_EPOCHS, learning_rate)
- 이 코드는 LSTM(Long-Short Term Memory) 모델을 사용하여 기존의 베이스라인 모델보다 더 나은 성능을 내기 위한 것입니다. 이를 위해 입력 윈도우의 너비와 레이블 윈도우의 너비를 정하고, train,validation, test 데이터셋을 분리하고, 이 데이터셋들을 정규화하고, 윈도우 생성 함수를 통해 모델에 입력할 데이터를 생성합니다.
- 그리고 모델은 1D합성곱(Conv1D)레이어와 LSTM 레이어로 구성되며, 마지막으로 Dense 레이어와 Reshape 레이어로 레이블 윈도우의 너비와 데이터셋의 피쳐 수를 조절해 최종 아웃풋을 생성합니다. 마지막으로 compile_and_fit 함수를 통해 모델을 컴파일하고 학습을 진행합니다.
pd.DataFrame(history.history).plot(figsize = (8, 5))
plt.title('Train and Val loss')
plt.show()
window.plot(model, plot_col = random.choice(train_df.columns))
5.2 Cross validation
교차 검증
We can see from the two plots above that the neural network seems to be able to learn. Let’s perform a cross validation to evaluate the results in details.
위의 두 개의 그림에서 우리는 신경망이 학습할 수 있는 것으로 보입니다. 자세한 결과를 얻기 위해 교차 검증을 수행해봅시다.
# Generate predictions for the test period taking the last values of the validation period
y_hat = model.predict(val_df.values[-input_width:, :].reshape(1, input_width, num_features))
predict_df = (pd.DataFrame(y_hat.reshape(label_width, num_features), columns=df.columns)*train_std + train_mean).assign(ds=date_time[-label_width:].values)
columns_to_keep = [e for e in predict_df.columns if '~' in e or e == 'ds']
predict_df = predict_df[columns_to_keep]
predict_df = predict_df.melt(id_vars =['ds'], value_vars =[c for c in predict_df.columns if c != 'ds'])
predict_df[['store_nbr', 'family']] = predict_df.key.str.split('~', expand=True)
predict_df = predict_df.rename(columns={'value': 'y_hat'})
predict_df['store_nbr'] = predict_df.store_nbr.astype('int')
predict_df.drop('key', axis=1, inplace=True)
predict_df = pd.merge(df_train.drop('id', axis=1), predict_df, on=['ds', 'store_nbr', 'family'], how='left')
predict_df['y_hat'] = np.clip(predict_df.y_hat, 0, np.inf)
predict_df.tail()
- 이 코드는 학습된 LSTM 모델을 사용하여 테스트 기간의 예측값을 생성하는 코드입니다. 먼저, 검증 기간의 마지막 값을 사용하여 입력값을 생성합니다. 그런 다음 모델을 사용하여 예측값을 생성하고, 예측 결과를 데이터프레임으로 변환합니다. 이후 예측 결과를 기존의 데이터프레임과 병합하고, ‘y_hat’열을 추가합니다. 마지막으로 ‘y_hat’ 값을 0 이상으로 clip하고, 예측 결과를 반환합니다.
Note that the cross-validation is performed from 2017-08-01 to 2017-08-15 to keep it simple. A more robust cross-validation would be to use a rolling window and compute the average of the metric over all those windows. See here for more details:
참고로, 교차 검증은 단순함을 유지하기 위해 2017-08-01에서 2017-08-15까지 수행됩니다. 더 견고한 교차 검증을 위해서는 롤링 윈도우를 사용하여 모든 윈도우에서 지표의 평균을 계산하는 것이 좋습니다. 자세한 내용은 여기를 참조하세요.
predict_df['error'] = (np.log(1 + predict_df[~predict_df.y_hat.isnull()].y) - np.log(1 + predict_df[~predict_df.y_hat.isnull()].y_hat))**2
print("RMSLE: %s" %rmsle(predict_df[~predict_df.y_hat.isnull()].y, predict_df[~predict_df.y_hat.isnull()].y_hat))
# 출력값: 0.5466
- 위 코드는 RMSLE를 계산하는 코드입니다. RMSLE는 예측값과 실제값의 차이를 로그 스케일로 변환한 값의 제곱에 대한 평균값의 제곱근으로, 값이 작을수록 예측 성능이 우수합니다.
5.3 Investigation
연구
Looking at the results above, it seems that the LSTM model is unable to outperformed the baseline model at least on the cross-validation period. Let’s try to understand where the weakness of the LSTM is coming from. A way to do that is to compute the evaluation metric for each of the time series in the test set and extract the highest values.
predict_df['key'] = predict_df['store_nbr'].astype(str) + '~' + predict_df['family'].astype(str)
rmsle_per_ts_df = predict_df.groupby('key').agg({'error': 'sum'}).sort_values('error', ascending=False).reset_index()
rmsle_per_ts_df.head(10)
- 위 코드는 test set의 각 시계열 데이터마다 평가 지표를 계산하고, 그 중에서 가장 높은 값을 추출하여 출력하는 코드입니다.
At a first look, SCHOOL AND OFFICE SUPPLIES family seems to have a major issue. Let’s a take a closer look by visualising the Time Series.
초반에 보면, ‘SCHOOL AND OFFICE SUPPLIES’ 가족의 경우 큰 문제가 있는 것으로 보입니다. 시계열 데이터를 시각화하여 더 자세히 살펴보겠습니다.
plot_ds_range = ['2015-05-01', predict_df.ds.iloc[-1]]
fig, ax = plt.subplots(figsize=(20, 5))
unique_keys = set(zip(df_train.store_nbr, df_train.family))
key = random.choice(list(unique_keys))
key = [48, 'SCHOOL AND OFFICE SUPPLIES']
ts = predict_df[(predict_df.store_nbr == key[0]) & (predict_df.family == key[1])]
ts = ts[(ts.ds >= plot_ds_range[0]) & (ts.ds <= plot_ds_range[1])]
# plt.plot(ts.ds, ts.onpromotion*np.median(ts.y), label='promotion')
plt.plot(ts.ds, ts.y_hat, label='y_hat')
plt.plot(ts.ds, ts.y, label='y')
plt.title("Store: " + str(key[0]) + ", Family: " + key[1])
plt.legend()
ax.set_xticks(np.array(ts.ds)[::100])
ax.tick_params(axis='x',rotation=45)
plt.show()
The issue is quite obvious. It looks like the LSTM model is not able to understand the yearly pattern. This is not a suprise as the model is trying to learn from the last 7 days to predict the next 14 days without any information of where we are in the year. I am currently working at injecting exogenous variables in the model in order to capture the seasonality patterns (such as day of the year, day of the week,…). If you have any recommendations on how to do that please let me know.
이 문제는 꽤 명백합니다. LSTM 모델이 연간 패턴을 이해하지 못하는 것 같습니다. 이는 모델이 다음 14일을 예측하기 위해 마지막 7일의 정보만을 가지고 있으며, 연도의 어느 지점에 있는지에 대한 정보가 전혀 없기 때문입니다. 저는 현재 모델에 외생 변수를 주입하여 (예: 연중의 날짜, 주중의 날짜 등) 계절성 패턴을 포착하려고 노력하고 있습니다. 이에 대해 추천할 만한 조언이 있으면 알려주세요.
6. Conclusion
This was a first attempt in order to use LSTMs for Time Series forecasting. The current LTSM model has a score 0.61 on the leaderboard (can vary depending on the run) and is unfortunately unable to beat the baseline model (0.52). My guess is that more information need to be feed into the model. To be continued…
이것은 시계열 예측을 위해 LSTM을 사용하는 첫 번째 시도였습니다. 현재의 LSTM 모델은 리더보드에서 0.61의 점수를 기록하고 (실행에 따라 다를 수 있음) 기준 모델(0.52)을 이길 수 없다는 것이 안타깝습니다. 내 추측은 모델에 더 많은 정보를 피드백해주어야 한다는 것입니다. 계속 진행 중입니다…
댓글남기기