[Tensorflow2] tf.data.Datasetを使って時系列TimeSeriesデータを学習データに加工する

#はじめに
DeepLearningのレジェンド的な存在であるAndrew NgとLaurence Moroneyが提供する[deeplearning.ai](https://www.deeplearning.ai/tensorflow-in-practice/)のコースにて、時系列データ(Time-series data)を学習モデルに流す前の下準備を[tf.data.Dataset](https://www.tensorflow.org/api_docs/python/tf/data/Dataset)を駆使して行っているのですが、この仕組みがいまいちわからなかったので、こちらに自分用のメモを兼ねて、また時系列データの分析をこれから行うような人のために、処理の手順を少し読み解いていこうと思います。
※まだ学習過程のため、解釈が違う場合があるので参考にする場合はご注意ください。

#環境について
この記事を書いたときのtensorflowのバージョンは2.4.1です。
分析は[GoogleColab](https://towardsdatascience.com/getting-started-with-google-colab-f2fff97f594c)上で行います。

#データについて
今回扱う時系列データは株データや気温データなどの、単変量(Univariate)なものを想定します。この記事では以下を使用します。

```python
series =  list(range(1, 10))
print(series)
#Result
#[1, 2, 3, 4, 5, 6, 7, 8, 9]
```

#STEPS
###1. Listデータをtf.data.Datasetに変換

```python
dataset = tf.data.Dataset.from_tensor_slices(series)
```

[tf.data.Dataset.from_tensor_slices](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#source_datasets_2)はListデータをdatasetに変換する最も簡単な方法です。変換後データの中身を確認するために`print()`するには以下のような工夫がいります。

```python
dataset = tf.data.Dataset.from_tensor_slices(series)
print(dataset)
#これだと、以下のようにObjectの情報がプリントされてしまう。
#<TensorSliceDataset shapes: (), types: tf.int32>
print(list(dataset.as_numpy_iterator()))
#これが一番単純。以下がプリントされる。
#[1, 2, 3, 4, 5, 6, 7, 8, 9]
for element in dataset:
  print(element)
#これだと各値が以下のようにプリントされる
#tf.Tensor(1, shape=(), dtype=int32)
#tf.Tensor(2, shape=(), dtype=int32)
#tf.Tensor(3, shape=(), dtype=int32)
#tf.Tensor(4, shape=(), dtype=int32)
#tf.Tensor(5, shape=(), dtype=int32)
#tf.Tensor(6, shape=(), dtype=int32)
#tf.Tensor(7, shape=(), dtype=int32)
#tf.Tensor(8, shape=(), dtype=int32)
#tf.Tensor(9, shape=(), dtype=int32)
#つまり、tf.data.datasetはtf.Tensorの集合体。
for element in dataset:
  print(element.numpy())
#tf.Tensorがnumpyに変換され、以下が出力される。
#1
#2
#3
#4
#5
#6
#7
#8
#9
```

###2. datasetをWindowDatasetに変換

`[1, 2, 3, 4, 5, 6, 7, 8, 9]`から学習用にデータがいくつのデータが取れますか？
基本的にNeuralNetworkは教師あり(Supervised)です。なので、これはデータの切り方によります。例えばこの時系列データから、`[1, 2, 3, 4, 5, 6, 7, 8]`をFeature、`[9]`をLabelと見立てれば、学習用データは1組のみになります。同様に`[1, 2, 3, 4]`をFeature、`[5]`をLabelと見立てることもできます。そうすると、このデータから学習用に以下の5組のデータが作れることになります。

```
[1, 2, 3, 4] [5]
[2, 3, 4, 5] [6]
[3, 4, 5, 6] [7]
[4, 5, 6, 7] [8]
[5, 6, 7, 8] [9]
```
この時、各組のFeatureをWindow、その長さ(この場合4)をwindow_sizeと呼びます。TensorflowではWindowDatasetクラスを使用してこれを実現できます。[tf.data.Dataset.window](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#window)を使って変換を行います。コードは以下になります。

```python
window_size = 4
dataset = dataset.window(window_size + 1, shift=1, drop_remainder=True) 
```

print結果は以下になります。

```python
print(dataset)
#　これだと、Objectの型を返すので以下が出力されます。
#　<WindowDataset shapes: DatasetSpec(TensorSpec(shape=(), dtype=tf.int32, name=None), TensorShape([])), types: DatasetSpec(TensorSpec(shape=(), dtype=tf.int32, name=None), TensorShape([]))>
for window in dataset:
  print(window)
# これだと、以下が出力されます。
# <_VariantDataset shapes: (), types: tf.int32>
# <_VariantDataset shapes: (), types: tf.int32>
# <_VariantDataset shapes: (), types: tf.int32>
# <_VariantDataset shapes: (), types: tf.int32>
# <_VariantDataset shapes: (), types: tf.int32>
#ここでわかるのは、WindowDatasetはVariantDatasetの集合体です。名前からtf.data.Datasetを継承していそうなので、as_numpy_iterator()が使えそう。
for window in dataset:
  print(list(window.as_numpy_iterator())
# 以下が出力されます。
# [1, 2, 3, 4, 5]
# [2, 3, 4, 5, 6]
# [3, 4, 5, 6, 7]
# [4, 5, 6, 7, 8]
# [5, 6, 7, 8, 9]

```

###3. datasetにflat_mapをかける。

今のままだと、WindowDatasetの中にVariantDatasetが含まれています。このVariantDatasetはTensorの集合体です。例えば、1つ目のVariantDatasetは5つのTensorを含み、各Tensorに1つの数字が入っています。
[tf.data.Dataset.flat_map](https://www.tensorflow.org/api_docs/python/tf/data/Dataset)では、このVariantDatasetを圧縮し、5つの数字の入ったTensorに変換します。また、圧縮することによりTensor内の数字のSequenceに意味合いを持たせることができるので、時系列データとして機械が認識できるようになります。以下、コードになります。

```python
dataset = dataset.flat_map(lambda window: window.batch(window_size + 1))
print(dataset)
# <FlatMapDataset shapes: (None,), types: tf.int32>
for element in dataset:
  print(element)
# ---Result---
# tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
# tf.Tensor([2 3 4 5 6], shape=(5,), dtype=int32)
# tf.Tensor([3 4 5 6 7], shape=(5,), dtype=int32)
# tf.Tensor([4 5 6 7 8], shape=(5,), dtype=int32)
# tf.Tensor([5 6 7 8 9], shape=(5,), dtype=int32)

```

###4. 時系列データをshuffleする
トレーニングデータを時系列順に流し込んで最適なModelを構築しようとすると、ModelはバッチごとにLoss functionとOptimizerに従って最適化されていくので、時系列によるバイアス(Sequence Bias)が入ってきます。これを避けるため、トレーニングデータを[tf.data.Dataset.shuffle](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#shuffle)を使い、シャッフルします。

```python
dataset = dataset.shuffle(1000)
for element in dataset:
  print(element)

# ---Result---
# tf.Tensor([5 6 7 8 9], shape=(5,), dtype=int32)
# tf.Tensor([3 4 5 6 7], shape=(5,), dtype=int32)
# tf.Tensor([2 3 4 5 6], shape=(5,), dtype=int32)
# tf.Tensor([1 2 3 4 5], shape=(5,), dtype=int32)
# tf.Tensor([4 5 6 7 8], shape=(5,), dtype=int32)
```

###5. データセットをFeatureとLabelに分ける

Datasetを変換するときは[tf.data.Dataset.map](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#map)を使用します。今回はこれを使ってデータセットをFeatureとLabelに分けます。

```python
dataset = dataset.map(lambda window: (window[:-1], window[-1]))
for el1, el2 in dataset:
  print(el1)
  print(el2)

# ---Result---
# tf.Tensor([3 4 5 6], shape=(4,), dtype=int32)
# tf.Tensor(7, shape=(), dtype=int32)
# tf.Tensor([2 3 4 5], shape=(4,), dtype=int32)
# tf.Tensor(6, shape=(), dtype=int32)
# tf.Tensor([4 5 6 7], shape=(4,), dtype=int32)
# tf.Tensor(8, shape=(), dtype=int32)
# tf.Tensor([1 2 3 4], shape=(4,), dtype=int32)
# tf.Tensor(5, shape=(), dtype=int32)
# tf.Tensor([5 6 7 8], shape=(4,), dtype=int32)
# tf.Tensor(9, shape=(), dtype=int32)
```

###6. Batchを指定する
次に、[tf.data.Dataset.batch](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#batch)でバッチを形成することで、複数のFeature・LabelのペアをModelに流し込みます。

```python
dataset = dataset.batch(5)
for el1, el2 in dataset:
  print(el1)
  print(el2)

# ---Result---
# tf.Tensor(
# [[1 2 3 4]
# [5 6 7 8]
# [4 5 6 7]
# [3 4 5 6]
# [2 3 4 5]], shape=(5, 4), dtype=int32)
# tf.Tensor([5 9 8 7 6], shape=(5,), dtype=int32)

```

###7. Datasetを前のバッチが処理されているときに次のバッチを読み込むように仕込む
[tf.data.Dataset.prefetch](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#prefetch)は、入力データセット加工を行うパイプラインの最後に設定するよう推奨されているメゾットです。アイドリングタイムを減らせるため、設定しておくとGPU処理が早くなると言われています。

```python
dataset = dataset.prefetch(1)
# 時間短縮のためだけに使われるため、出力結果は変わらない
```

#最後に

以上がデータ加工のステップでした。
加工後、トレーニングデータを`model.fit`のパラメターにセットし、Modelを最適化しましょう。[deeplearning.ai](https://www.deeplearning.ai/tensorflow-in-practice/)ではここら辺の処理を5分ぐらいで説明されたので少し置いておきぼりになりました。この記事を書いていくことで自分である程度納得できるようになったので、良いきっかけになりました。