Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法
はじめに
カブクで機械学習エンジニアをしている大串正矢です。今回は複数時系列データを1つの深層学習モデルで学習させる方法について書きます。
背景
複数時系列データは複数企業の株価の変動、各地域における気温変動、複数マシーンのログなど多岐に渡って観測できます。この時系列ごとにモデルを用意して管理するとなると学習、運用において多大なるコストがかかってしまいます。1つのモデルで複数の時系列を管理できれば運用が楽になるだけでなく、学習も1度で済むのでトライアンドエラーの工数も大幅に削減できます。
本記事ではそのために実現可能な2つの手法を紹介します。
複数時系列データを1つの深層学習モデルで学習させる方法
Kerasで複数の時系列データを深層学習モデルで学習させる手法には2つあります。
- 複数入力型の深層学習モデル
- 個別入力型の深層学習モデルの組み合わせ
1の手法の利点はモデルがシンプルなので学習と予測が2の手法に比べ高速になります。
2の手法の利点は時系列ごとにカスタマイズ可能なので1よりも精度を高めることが容易になります。
複数入力型の深層学習モデル
データ
今回は下記のレポジトリのデータをいくつかピックアップして使用します。
https://github.com/jamesrobertlloyd/gpss-research/tree/master/data/tsdlr_9010
下記の2つのデータをピックアップしてモデルを作成します。
- 気温変化のデータ
- ガスの生産量のデータ
前処理
データを前処理をして学習が容易な形にします。まずはデータを取得し対数化します。これはガスの生産量はデータのスケールが1646から約6000近くまであるのに対し、気温変化は-0.8から26.3とデータ間の差が大きいためです。
- データをリンク先から読み込みます。
- データのスケールが異なるので両データを対数にスケーリングします。
- 0の部分は計算できないので + 1の処理が入ります。
- それでも発生する計算できない値はnanになるため0に置き換えます。
pythonコードでは下記のようになります。
wave_data = read_csv('https://raw.githubusercontent.com/jamesrobertlloyd/gpss-research/master/data/tsdlr_5050/daily-minimum-temperatures-in-me-train.csv', header=None, names=["Date", "Temp"])
wave_data = wave_data.sort_values(by=['Date'])
production_of_gas_data = read_csv('https://raw.githubusercontent.com/jamesrobertlloyd/gpss-research/master/data/tsdlr_5050/monthly-production-of-gas-in-aus-train.csv', header=None, names=["Date", "production-of-gas"])
production_of_gas_data = production_of_gas_data.sort_values(by=['Date'])
X_orig = np.nan_to_num(np.log(wave_data["Temp"].values + 1))
X_day = wave_data["Date"].values
X_orig_second = np.nan_to_num(np.log(production_of_gas_data["production-of-gas"].values + 1))
X_day_second = production_of_gas_data["Date"].values
対数スケーリング済みデータ
図1. 気温の変化
図2. ガスの生産量
データの分割
学習データとテストデータに分割し、複数入力に使用可能なように学習データを合わせます。
X_train_joint = np.vstack((X_train, X_train_second))
X_test_joint = np.vstack((X_test, X_test_second))
学習データの正規化及びwindow幅に合わせたデータ設定
学習データを正規化し、window幅に合わせたデータセットに変更します。正規化することで学習が容易になり、window幅は3にしています。
def get_data(data, time_steps: int=3):
dataX = []
print(data.shape)
dataX = np.zeros((data.shape[0], data.shape[1], time_steps))
for i in range(data.shape[0]):
for j in range(data.shape[1] - time_steps - 1):
dataX[i][j] = data[i, j:(j + time_steps)].T
return np.array(dataX)
def transform_data(original_data: np.array,
inverse_option: bool,
scaler: object,
variable_number: int,
):
data_shape = original_data.shape
print(original_data.shape)
data = original_data.reshape(-1, variable_number)
print(data.shape)
if inverse_option is True:
print('before max {}'.format(max(data[0])))
print('Inverse')
data = scaler.inverse_transform(data)
print('after max {}'.format(max(data[0])))
else:
print('before max {}'.format(max(data[0])))
print('Normalize')
data = scaler.fit_transform(data)
print('after max {}'.format(max(data[0])))
data = data.reshape(data_shape)
return data, scaler
def prepare_data(original_data, time_steps, variable_number):
copy_data = original_data.copy()
scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
data, scaler = transform_data(original_data=copy_data,
inverse_option=False, scaler=scaler, variable_number=variable_number)
data = np.asarray(data)
x = get_data(data, time_steps=time_steps)
print(x.shape)
x = np.swapaxes(x, 0, 1)
x = np.swapaxes(x, 1, 2)
return x, scaler
time_steps = 3
variable_number = 2
x, scaler = prepare_data(X_train_joint, time_steps, variable_number)
input_dim = x.shape[-1]
timesteps = x.shape[1]
モデルの定義部分
下記でモデルの定義をします。input_dim
がデータの入力種類を表しています。今回は2種類のデータなので2が入ります。
def create_model(input_dim,
time_steps,
latent_dim,
# データが一つしかないので1しか選べない
batch_size=1,
model_option='lstm',
optimizer='adam',
):
with tf.name_scope('Model'):
x = Input(shape=(time_steps, input_dim,))
if model_option == 'lstm':
with tf.name_scope('LSTM'):
h = LSTM(latent_dim, stateful=False, return_sequences=True)(x)
elif model_option == 'gru':
with tf.name_scope('GRU'):
h = GRU(latent_dim, stateful=False, return_sequences=True)(x)
print('input_dim:', input_dim)
with tf.name_scope('Dense'):
out = Dense(input_dim)(h)
model = Model(x, out)
model.summary()
with tf.name_scope('ModelCompile'):
model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])
return model
定義されたモデルは下記のようになります。1つのモデルで複数の時系列データを扱っているのでLSTMモデルは1つだけになります。
モデルの予測部分
モデルが学習した結果を用いて適切に予測できるかを確認します。正規化したデータを元のスケールに戻す処理を入れています。
def predict_model_show_graph(day, x, scaler, model, variable_number):
prepare_value = x.copy()
preds = model.predict(prepare_value)
print('prepare_value: ', prepare_value.shape)
print('preds: ', preds.shape)
x_scale, scaler = transform_data(original_data=prepare_value, inverse_option=True,
scaler=scaler, variable_number=variable_number)
predict_scale, scaler = transform_data(original_data=preds, inverse_option=True,
scaler=scaler, variable_number=variable_number)
for i in range(prepare_value.shape[-1]):
plt.figure(figsize=(14, 8))
print('x_scale: ', prepare_value.shape)
plt.plot(prepare_value[:, 0, i], color='r', label='data')
plt.plot(preds[:, 0, i], color='b', label='predict')
plt.legend()
plt.show()
print('model evaluate mse:', model.evaluate(preds, prepare_value))
return preds, prepare_value
モデルの学習
下記のコードでモデルの学習、予測結果の確認を行います。
predict_list = []
var_list = []
print(max_weight)
print(x.shape)
model = create_model(input_dim,
time_steps=time_steps,
latent_dim=120,
model_option='lstm',
)
log_dir = 'simple_multi_model'
p = Path(log_dir)
p.mkdir(parents=True, exist_ok=True)
tensorboard = TensorBoard(log_dir=log_dir,
write_graph=True,
embeddings_freq=0,
)
with timer('train model simple'):
model.fit(x, x, epochs=400, callbacks=[tensorboard])
window = time_steps
x_test, scaler = prepare_data(X_test_joint, time_steps, variable_number)
predict_test, x_scale_test = predict_model_show_graph(X_test_day[window + 1:],
x_test, scaler, model,
variable_number)
予測結果
テストデータに対する予測結果は下記のようになります。
気温に関する実データ(赤)と予測データ(青)
ガスの生産量に関する実データ(赤)と予測データ(青)
実データと予測データの差の指標としてRMSEを用いました。この値が小さいほど精度が高いモデルになります。
値は下記になります。
データ | RMSE |
---|---|
気温 | 0.0341 |
ガスの生産量 | 0.0910 |
個別入力型の深層学習モデルの組み合わせ
モデル定義
コードは下記のようになります。
下記のコードの場合はlist型のデータセットに時系列ごとに設定するLSTMモデルごとのノード数を入れておけば個別に設定が可能になります。時系列ごとにモデルを変更しても構いませんが今回はシンプルにするため、モデルのパラメータのみ変更しています。
def create_model_individual(
input_dim,
time_steps,
latent_dim_list,
# データが一つしかないので1しか選べない
batch_size=1,
model_option='lstm',
optimizer='adam',
):
input_list = []
output_list = []
with tf.name_scope('Model'):
for i in range(input_dim):
x = Input(shape=(time_steps, 1,))
if model_option == 'lstm':
with tf.name_scope('LSTM' + str(i)):
h = LSTM(latent_dim_list[i], stateful=False, return_sequences=True)(x)
elif model_option == 'gru':
with tf.name_scope('GRU' + str(i)):
h = GRU(latent_dim_list[i], stateful=False, return_sequences=True)(x)
with tf.name_scope('Dense' + str(i)):
out = Dense(1)(h)
input_list.append(x)
output_list.append(out)
model = Model(inputs=input_list, outputs=output_list)
model.summary()
with tf.name_scope('ModelCompile'):
model.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mse'])
return model
定義されたモデルは下記のようになります。時系列ごとにLSTMが用意されていますが全体のモデルとしては1つになります。
データの前処理
モデルが異なるので入力するデータのフォーマットが異なります。(2, 150, 3, 1)
(データの種類、バッチサイズ、window幅、lstmに入力するデータの種類)のサイズのデータをlist
型にして返しています。
def prepare_data_individual(original_data, time_steps, variable_number):
copy_data = original_data.copy()
scaler = MinMaxScaler(feature_range=(0, 1), copy=False)
data, scaler = transform_data(original_data=copy_data,
inverse_option=False, scaler=scaler, variable_number=variable_number)
data = np.asarray(data)
x = get_data(data, time_steps=time_steps)
x_reshape = []
for i in range(x.shape[0]):
x_reshape.append([x[i]])
x_reshape = np.array(x_reshape)
x_reshape = np.swapaxes(x_reshape, 1, 2)
x_reshape = np.swapaxes(x_reshape, 2, 3)
return list(x_reshape), scaler
x, scaler = prepare_data_individual(X_train_joint, time_steps, variable_number)
keras2.1.5のバージョンではデータの長さを下記でチェックしており、numpyのarray式でデータを与えるとデータのサイズチェックエラーが発生してしまいます。下記のコードを避けるためにデータの本質は同じですがlist
形式にしています。
if len(data) != len(names):
if data and hasattr(data[0], 'shape'):
raise ValueError(
'Error when checking model ' + exception_prefix +
': the list of Numpy arrays that you are passing to '
'your model is not the size the model expected. '
'Expected to see ' + str(len(names)) + ' array(s), '
'but instead got the following list of ' +
str(len(data)) + ' arrays: ' + str(data)[:200] + '...')
モデルの学習
モデルの学習に使用したコードは下記になります。時系列ごとのlstmの出力空間の次元を変更しています。
predict_list = []
var_list = []
model = create_model_individual(input_dim,
time_steps=time_steps,
latent_dim_list=[120, 150],
model_option='lstm',
)
log_dir = 'model_individual'
p = Path(log_dir)
p.mkdir(parents=True, exist_ok=True)
tensorboard = TensorBoard(log_dir=log_dir,
write_graph=True,
embeddings_freq=0,
)
with timer('train model individual'):
model.fit(x, x, epochs=400, callbacks=[tensorboard])
window = time_steps
x_test, scaler = prepare_data_individual(X_test_joint, time_steps, variable_number)
predict_test, x_scale_test = predict_model_show_graph_individual(X_test_day[window + 1:],
x_test, scaler, model,
variable_number)
予測結果
手法1と同様にRMSEで評価しました。
データ | RMSE |
---|---|
気温 | 0.0174 |
ガスの生産量 | 0.0732 |
比較結果
精度と学習速度、予測速度を比較します。
- 実行環境
- OS: macOS Sierra
- CPU: 2.9 GHz Intel Core i7
- メモリー: 16 GB 2133 MHz LPDDR3
- pythonバージョン
- 3.6.0
- ライブラリ
numpy==1.14.2
ipython==6.2.1
notebook==5.4.1
pandas==0.22.0
statsmodels==0.8.0
matplotlib==2.2.2
lxml==4.2.0
bs4==0.0.1
scikit-learn==0.19.1
scipy==1.0.0
keras==2.1.5
tensorflow==1.7.0
- モデル
- LSTM
- ノード数:120, 個別入力は気温変化が120, ガスの生産量は150
- 他のパラメータは
kearas
で提供されているデフォルト値
- Optimiser
- adam
他のパラメータはkearas
で提供されているデフォルト値
- adam
- epoch
- 400
モデル | 平均RMSE | 学習時間(400 epoch) | 予測時間 |
---|---|---|---|
複数入力型の深層学習モデル | 0.0625 | 15.620秒 | 0.52秒 |
個別入力型の深層学習モデルの組み合わせ | 0.0453 | 23.065秒 | 0.693秒 |
平均RMSEは’個別入力型の深層学習モデルの組み合わせ’の方が約37%向上しています。(低い方が良い値です。)
学習時間は約33%遅くなり、予測時間は約33%遅くなっています。
このように各手法は精度と速度のトレードオフになるので用途に合わせて使用を使い分けて下さい。
今回使用したコードは下記になります。
最後に
Kerasを使いこなせればこのような実装も楽にできます。Kerasの実装や時系列データに興味があるエンジニアがいらっしゃれば絶賛採用中なので是非、弊社へ応募してください。
参考
https://machinelearningmastery.com/multivariate-time-series-forecasting-lstms-keras/
その他の記事
Other Articles
関連職種
Recruit